FNSP Web Page

Context: o que resolve, e o render que provoca sem se dar por ele

O Context resolve um problema: entregar um valor a um componente fundo na árvore sem o passar à mão por todos os que estão pelo caminho. Não é mais do que isso — e sobretudo não é gestão de estado. É dessa confusão que vem quase tudo o que corre mal com ele.

O estado continua a viver num useState acima. O Context é só o túnel.

1. O render que ele provoca

Quando o valor do Context muda, todos os componentes que o leem voltam a renderizar. Não os que usam a parte que mudou: todos. O React não tem como saber que pedaço do valor interessa a cada um — para ele o valor é um só, e comparou-o por referência.

E o memo não salva ninguém:

const Avatar = memo(function Avatar() {
  const { utilizador } = useContext(ContextoSessao);
  return <img src={utilizador.foto} />;
});

Aquele memo compara props, e o valor do Context não chega como prop — entra por dentro. O componente volta a renderizar sempre que o Context mudar, com memo ou sem ele. É um dos casos em que ele é só ruído, como no artigo do useMemo e do useCallback.

2. A armadilha do objeto novo

// A cada render deste componente nasce um objeto novo
<ContextoSessao.Provider value={{ utilizador, entrar, sair }}>

O utilizador pode ser exactamente o mesmo, e entrar e sair podem fazer exactamente o que faziam. O objeto que os embrulha é que é novo — e é ele que o React compara. Resultado: o Context «mudou», e todos os consumidores renderizam por nada.

A correção é dar estabilidade ao valor:

const valor = useMemo(
  () => ({ utilizador, entrar, sair }),
  [utilizador, entrar, sair],
);

Com uma ressalva que se percebe mal: isto só ajuda quando o provider renderiza por outros motivos. Se foi o utilizador que mudou, os consumidores renderizam — e devem. O useMemo não evita renders necessários; evita os que não eram.

3. Dividir em dois

A divisão que resolve o problema a sério não é um useMemo: é separar o que muda do que não muda.

<ContextoUtilizador.Provider value={utilizador}>   {/* muda a cada login */}
  <ContextoAcoes.Provider value={acoes}>           {/* nasce uma vez */}
    {filhos}
  </ContextoAcoes.Provider>
</ContextoUtilizador.Provider>

Um botão de «sair» só precisa da ação. Lendo apenas o segundo Context, nunca volta a renderizar quando o utilizador muda — porque o que ele lê não mudou. É o mesmo raciocínio das chaves estáveis: o que importa é o React conseguir distinguir o que é o mesmo do que é outro.

4. O caso deste site: o tema, sem Context nenhum

Este site não tem um único createContext. Não é posição de princípio — é que o sítio onde ele era óbvio acabou resolvido de outra maneira, e as razões valem mais do que a regra.

O alternador de tema é o exemplo de manual do Context. Só que aqui a verdade sobre o tema não está no React: está numa classe no <html>, posta por um guião em linha que corre antes de a página ser pintada. Sem isso, o site desenha-se claro e só depois escurece — um clarão branco a cada navegação, precisamente a quem escolheu o escuro.

Com um Context passava a haver duas verdades sobre a mesma coisa: a classe no <html> e o estado do React. Duas verdades divergem sempre, e divergem em silêncio.

const escuro = useSyncExternalStore(
  subscrever,
  estaEscuroNoCliente,
  estaEscuroNoServidor,
);

O useSyncExternalStore é o que serve para ler uma fonte de verdade que vive fora da árvore, em vez de a copiar para dentro. O subscrever observa a classe com um MutationObserver, e por isso o botão acerta mesmo quando o tema é mudado noutro separador.

Há ainda uma terceira razão, a que mais pesa num projeto com App Router: um provider obriga a 'use client'. O Context não atravessa a fronteira entre o servidor e o cliente, e pô-lo no topo arrasta para o browser a árvore toda que ele embrulha — exactamente o contrário do que se foi lá buscar, como no artigo dos Server Components.

5. Duas coisas que o 19 mudou

  • O provider passou a ser o próprio Context: <Contexto value={...}> em vez de <Contexto.Provider value={...}>.

  • use(Contexto) pode ser lido dentro de um if, o que o useContext nunca permitiu.

Ambas ficaram de fora do artigo da migração do 18 para o 19 por serem pequenas, e são das poucas mudanças daquela versão que não partem nada.

O resumo

O Context serve para não passar um valor à mão por dez componentes. Enquanto o valor mudar pouco — o tema, o idioma, a sessão — é a ferramenta certa. A partir do momento em que muda a cada tecla, está-se a pedir à árvore inteira que renderize por causa de uma letra, e o problema deixou de ser de Context.

E antes de criar um: ver se a verdade não está já fora do React. Se estiver, o que falta não é um Context — é uma forma de a ler.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.