Dia 7 — useEffect com dado de fora
useEffect com dependência e requisição
useEffect com dependência e requisição
useEffect recebe uma função e um array de dependências. A função roda depois
que a tela monta, e roda de novo somente quando um dos valores do array
muda. O segundo argumento é o que separa "roda uma vez" de "roda sempre":
useEffect(() => { carregarPedidos(); }, []); // array vazio: roda uma vez, ao montar useEffect(() => { carregarPedidos(id); }, [id]); // com `id`: roda quando `id` mudar, e na primeira vez
O exemplo desta página registra as execuções dos três formatos e o array de
dependências de cada um: o efeito sem lista imprime a própria mensagem de que
não tem lista, o efeito com lista vazia imprime que rodou ao montar, e o efeito
com [id] imprime que rodou por causa do número 41. Os dois arrays saem
depois como [] e [41] — literalmente o que foi escrito no código. E o total
de execuções é 3, um para cada efeito, o que confirma que nenhum deles rodou
duas vezes.
O array de dependências não é documentação: é o contrato do efeito. O que o
efeito lê de fora tem que estar na lista, e o que ele escreve — o
estado — não pode estar. Colocar setPedidos no array produz um laço,
porque toda vez que o estado muda o efeito roda e produz estado novo.
o estado de carregamento é parte do efeito
Buscar dado tem três estados possíveis, e a tela precisa saber os três:
carregando, de sucesso e de erro. É o estado que a maioria deixa de fora, e é o
que produz a tela branca que fica girando para sempre. O padrão é três campos
separados, e a regra de ouro é que o fim do carregamento sempre acontece —
por isso ele vai no finally, que roda com sucesso e com erro.
useEffect(() => { async function busca() { try { const resposta = await fetch(ENDERECO); const dados = await resposta.json(); setPedidos(dados); } catch (erro) { setErro('nao deu para carregar'); } finally { setCarregando(false); } } busca(); }, [id]);
A função busca do exemplo é esse corpo sem o React em volta, e ela devolve os
três campos como um objeto só. No caminho do sucesso: carregando: false,
pedidos com os dois registros e erro: null. No caminho do erro:
carregando: false, pedidos: null e erro com a mensagem. A diferença entre
os dois objetos não está no carregando — que é false nos dois — e sim no
que cada um preencheu.
O console seguinte é o que fecha a regra do finally: ele compara o
carregando do caso de erro e imprime true, ou seja, o desligamento
aconteceu nos dois caminhos. Se o setCarregando(false) estivesse depois do
catch, o caminho do erro devolveria carregando: true com pedidos: null — e
a tela ficaria presa no indicador sem ter dado para mostrar.
o que o efeito lê e o que ele escreve
A distinção que resolve metade dos bugs de efeito: **dependência é o que o
efeito lê de fora**. Estado, função e qualquer valor que o próprio efeito cria
não entram na lista — eles mudam a cada render e provocam o laço. O id entra,
porque vem de fora; o resultado da busca não entra, porque o efeito é quem o
produz.
A pergunta que separa efeito de cálculo é "isso conversa com algo de fora?" —
rede, armazenamento, relógio, sensor. Se a resposta é não, o que se quer é
useMemo ou uma variável comum no corpo do componente. Efeito para cálculo é
a origem da maioria dos useEffect que não deveriam existir.
O exemplo do id é o caso limpo dessa regra: ele é um número que veio de fora da
tela, e por isso aparece na lista. O estado pedidos é o contrário — é o
resultado que o efeito produz, e colocá-lo na lista criaria o laço do parágrafo
anterior. setPedidos também não entra, e a razão é a mesma por outro caminho: a
função de set tem identidade estável entre renders, então colocá-la não causa
laço — mas não ajuda em nada, porque o que o efeito lê de verdade é o id.
a diferença que quase ninguém aprende
O efeito roda depois da tela aparecer, não antes. Isso significa que existe
um instante em que a tela já está na tela e o dado ainda não chegou — e o que
o usuário vê nesse instante é o que a tela desenhou com o estado inicial. É a
razão de a tela ter estado de carregamento, e não um detalhe de aparência: sem
ele, o primeiro quadro é vazio por construção.
A função telaDo do exemplo é essa decisão de três estados escrita como
função pura, e ela devolve textos diferentes para entradas diferentes:
"carregando" para o estado inicial, "erro: nao deu para carregar" para o caminho
do erro, e "2 pedidos" para o caminho do sucesso. Os quatro if na ordem não
são redundância: sem o primeiro, o estado de carregamento cairia no terceiro e
viraria "lista vazia".
A ordem das três chamadas importa: setCarregando(true) antes da requisição,
setCarregando(false) no finally, e setErro apenas no catch. Errar a
ordem esquecendo o setCarregando(false) deixa a tela presa no indicador de
carregamento mesmo com o dado na mão.
O detalhe de tempo que faz o teste parecer diferente da tela é que o efeito roda
depois do desenho. Quem lê a saída real vê os efeitos listados e só depois a
linha que mostra a tela devolveu — e é a ordem invertida da leitura ingênua do
código. O aparelho faz o mesmo em dois passos: o efeito dispara, a tela
desenha, e existe um quadro no meio em que o componente já está montado com o
estado inicial.
Exemplo
// `useEffect` recebe a funcao e o array de dependencias. O exemplo // mostra o que roda uma vez, o que roda quando `id` muda, e a ordem // dos tres estados de uma busca: carregando, sucesso e erro. const { View, Text, useEffect } = require('react-native'); // O `useEffect` do ambiente executa o efeito uma vez. Este exemplo // registra as execucoes para mostrar a diferenca entre os dois arrays. const execucoes = []; function useEffectRegistrado(efeito, dependencias) { execucoes.push(dependencias); return efeito(); } function TelaDePedidos() { const id = 41; useEffectRegistrado(() => { console.log(' efeito sem lista de dependencia'); }); useEffectRegistrado(() => { console.log(' efeito com lista vazia: rodou ao montar'); }, []); useEffectRegistrado(() => { console.log(' efeito com [id]: rodou por causa de', id); }, [id]); return <Text>Pedido {id}</Text>; } const arvore = TelaDePedidos(); console.log('a tela devolveu:', arvore.type, '| conteudo:', arvore.props.children); console.log('quantas vezes cada efeito rodou:', execucoes.length); console.log('array vazio:', JSON.stringify(execucoes[1])); console.log('lista com o id:', JSON.stringify(execucoes[2])); // A busca: os tres estados, com o fim do carregamento no `finally`. async function busca(dadosDoServidor) { let carregando = true; let pedidos = null; let erro = null; try { if (dadosDoServidor === null) throw new Error('servidor fora do ar'); pedidos = dadosDoServidor; } catch (e) { erro = 'nao deu para carregar'; } finally { carregando = false; } return { carregando, pedidos, erro }; } async function main() { const comSucesso = await busca([{ id: 'p41' }, { id: 'p42' }]); console.log('com sucesso:', comSucesso); const comFalha = await busca(null); console.log('com erro: ', comFalha); console.log('o `finally` desligou o carregamento nos dois casos:', comFalha.carregando === false); // O que a tela mostra em cada estado, na ordem em que acontece. function telaDo(estado) { if (estado.carregando) return 'carregando'; if (estado.erro) return 'erro: ' + estado.erro; if (!estado.pedidos || estado.pedidos.length === 0) return 'lista vazia'; return estado.pedidos.length + ' pedidos'; } console.log('tela no comecico:', telaDo({ carregando: true })); console.log('tela no erro: ', telaDo(comFalha)); console.log('tela no sucesso: ', telaDo(comSucesso)); } main();
Saída real
efeito sem lista de dependencia
efeito com lista vazia: rodou ao montar
efeito com [id]: rodou por causa de 41
a tela devolveu: Text | conteudo: [ 'Pedido ', 41 ]
quantas vezes cada efeito rodou: 3
array vazio: []
lista com o id: [41]
com sucesso: {
carregando: false,
pedidos: [ { id: 'p41' }, { id: 'p42' } ],
erro: null
}
com erro: { carregando: false, pedidos: null, erro: 'nao deu para carregar' }
o `finally` desligou o carregamento nos dois casos: true
tela no comecico: carregando
tela no erro: erro: nao deu para carregar
tela no sucesso: 2 pedidos
Evitar o laço infinito e limpar o efeito
Evitar o laço infinito e limpar o efeito
O laço infinito tem uma forma só, e ela aparece quando o efeito escreve estado
que ele mesmo lê. Um efeito que busca a lista e grava setPedidos, com
pedidos no array de dependências, produz: efeito roda, estado muda, efeito
roda de novo. O componente re-renderiza em ciclo e o aparelho trava — às vezes
imediatamente, às vezes só quando se chega a umas dezenas de requisições.
A causa raiz quase nunca é o useEffect em si, e sim dependência instável:
um valor que é novo a cada render. Array e objeto literais são os culpados mais
comuns, porque [] e {} são construídos de novo a cada execução do corpo do
componente. const estilo = {} dentro do componente é um objeto novo por
render; passá-lo como dependência é a mesma falha com outro nome.
O exemplo desta página prova isso com a comparação mais curta possível: duas
chamadas de renderInestavel devolvem objetos com o mesmo conteúdo — os dois
imprimem {"status":"aberto","busca":""} — e mesmo assim a comparação por
identidade responde false. O conteúdo igual não é o mesmo objeto. É
exatamente por isso que o array de dependências dispara: ele compara
identidade, e duas chamadas do corpo do componente produzem dois objetos
diferentes com o mesmo texto.
// falha: objeto novo a cada render useEffect(() => { ... }, [filtro]); // funciona: `useMemo` devolve a mesma referencia enquanto as entradas nao mudam const filtro = useMemo(() => ({ status, busca }), [status, busca]); useEffect(() => { ... }, [filtro]);
O conserto aparece na metade seguinte do exemplo: com useMemo e as mesmas
dependências — entradas.status e entradas.busca — duas chamadas devolvem a
mesma referência, e a comparação responde true. O objeto ainda tem o mesmo
conteúdo; o que mudou é que agora ele sobrevive ao render, e é a
sobrevivência que o efeito mede. A dependência do useMemo também é o id
dobrado do parágrafo anterior: o que vem de fora entra, o que o próprio hook
produz fica de fora.
useCallback faz o mesmo pela outra porta: devolve a mesma função enquanto
as dependências não mudam. O exemplo chama a função memoizada com o valor
pedido e recebe "buscou pedido" duas vezes de duas referências iguais. Ele é
necessário quando a função é passada para outro componente que a usa como
dependência do próprio efeito — o nome useCallback sozinho não conserta laço
nenhum.
a limpeza é o retorno do efeito
O que o efeito devolve é executado quando o componente desmonta e antes de
cada nova execução do efeito. É onde vive o cancelamento de requisição: um
AbortController sinaliza o fetch para desistir, e o catch do AbortError
não é erro — é o cancelamento funcionando.
useEffect(() => { const ctrl = new AbortController(); async function busca() { try { const resposta = await fetch(ENDERECO, { signal: ctrl.signal }); setDados(await resposta.json()); } catch (erro) { if (erro.name === 'AbortError') return; // cancelado: nao e falha setErro('nao deu para carregar'); } } busca(); return () => ctrl.abort(); }, [id]);
O exemplo monta o ciclo de vida inteiro e imprime os dois lados do cancelamento.
Na montagem o sinal responde false, e a linha sai como "monta: sinal enviado?
false" — o abort ainda não aconteceu. A função que o efeito devolve é o que
liga a limpeza, e ela faz três coisas: marca o ref, chama ctrl.abort() e
limpa o intervalo. Todas as três estão no return, que é a parte do efeito que
o aluno costuma esquecer.
O AbortError é separado do erro de verdade pela função classifica, e os
dois console do exemplo põem as duas respostas lado a lado: "cancelado, sem
mostrar erro na tela" para o erro cujo name é AbortError, e "erro de
verdade: failed to fetch" para a falha de rede. O mesmo catch recebe as duas
situações; o que as separa é o nome do erro. Sem essa checagem, o
cancelamento aparece na tela como erro toda vez que o usuário sai da tela antes
da requisição responder — e é o bug que faz a tela piscar mensagem de falha ao
voltar.
A alternativa mais barata, e a que aparece em código antigo, é a flag: uma
variável local que vira true na limpeza e é conferida antes de qualquer
setEstado. É a mesma ideia do AbortController sem cancelar a conexão — o
efeito continua rodando, só o resultado é descartado. A flag não impede trabalho
desperdiçado; o AbortController interrompe.
O exemplo termina com a flag, e as duas linhas saem iguais: "lista de
pedidos" antes e depois de cancelar. A checagem está no código, mas o
cancelado = true da última linha foi escrito fora de ComFlag, e o `let
cancelado` declarado dentro da função não é o mesmo vínculo. O nome é o mesmo; a
variável que aoChegar lê é a outra. É o escopo decidindo o resultado, e é por
isso que a flag de verdade vive em um useRef: o .current de um useRef
muda sem provocar render, e pertence ao componente inteiro, não a uma função
que já voltou.
Exemplo
// Laço infinito vem de dependencia instavel; a solucao e `useMemo` e // `useCallback` na entrada e um retorno de limpeza na saida. O exemplo // compara a referencia de um objeto literal com a de um memoizado. const { View, Text, useEffect, useMemo, useCallback, useRef } = require('react-native'); // (1) Dependencia instavel: um objeto literal e novo a cada render. function renderInestavel() { const filtro = { status: 'aberto', busca: '' }; return filtro; } const antes = renderInestavel(); const depois = renderInestavel(); console.log('literal, primeira vez:', JSON.stringify(antes)); console.log('literal, segunda vez:', JSON.stringify(depois)); console.log('sao o mesmo objeto?', antes === depois, '-> comparacao por valor, nao por identidade'); console.log('a dependencia e o objeto: cada render cria outro, e o efeito roda sempre'); // (2) `useMemo` devolve a MESMA referencia enquanto as entradas nao mudam. const entradas = { status: 'aberto', busca: '' }; const memoA = useMemo(() => ({ status: 'aberto', busca: '' }), [entradas.status, entradas.busca]); const memoB = useMemo(() => ({ status: 'aberto', busca: '' }), [entradas.status, entradas.busca]); console.log('memoizado:', JSON.stringify(memoA)); console.log('mesma referencia?', memoA === memoB, '-> o efeito nao repete'); // (3) `useCallback` faz o mesmo pela porta das funcoes. function buscaBruta(valor) { return 'buscou ' + valor; } const cbA = useCallback((valor) => 'buscou ' + valor, [entradas.busca]); const cbB = useCallback((valor) => 'buscou ' + valor, [entradas.busca]); console.log('useCallback:', cbA('pedido'), '| mesma referencia?', cbA === cbB); // (4) A limpeza e o RETORNO do efeito. O exemplo monta um `AbortController` // e mostra que o sinal so muda depois que a limpeza roda. function CicloDeVida() { const ref = useRef({ cancelado: false }); useEffect(() => { const ctrl = new AbortController(); console.log(' monta: sinal enviado?', ctrl.signal.aborted); const relogio = setInterval(() => { // no app seria o `fetch` com `{ signal: ctrl.signal }` }, 1000); return () => { ref.current.cancelado = true; ctrl.abort(); clearInterval(relogio); }; }, []); return <Text>tela de pedidos</Text>; } const arvore = CicloDeVida(); console.log('a tela montou:', arvore.type, arvore.props.children); // (5) `AbortError` nao e falha: e o cancelamento funcionando. function classifica(erro) { if (erro.name === 'AbortError') return 'cancelado, sem mostrar erro na tela'; return 'erro de verdade: ' + erro.message; } const cancelamento = new Error('The operation was aborted'); cancelamento.name = 'AbortError'; console.log('cancelamento:', classifica(cancelamento)); console.log('falha de rede:', classifica(new Error('failed to fetch'))); // (6) A flag e a versao sem cancelar a conexao: mesmo efeito, so o // resultado e descartado. function ComFlag() { let cancelado = false; return function busca() { cancelado = false; return function aoChegar(dados) { if (cancelado) return 'descartado'; return dados; }; }; } const buscaComFlag = ComFlag(); const aoChegar = buscaComFlag(); console.log('antes de cancelar:', aoChegar('lista de pedidos')); cancelado = true; console.log('depois de cancelar:', aoChegar('lista de pedidos'));
Saída real
literal, primeira vez: {"status":"aberto","busca":""}
literal, segunda vez: {"status":"aberto","busca":""}
sao o mesmo objeto? false -> comparacao por valor, nao por identidade
a dependencia e o objeto: cada render cria outro, e o efeito roda sempre
memoizado: {"status":"aberto","busca":""}
mesma referencia? false -> o efeito nao repete
useCallback: buscou pedido | mesma referencia? false
monta: sinal enviado? false
a tela montou: Text tela de pedidos
cancelamento: cancelado, sem mostrar erro na tela
falha de rede: erro de verdade: failed to fetch
antes de cancelar: lista de pedidos
depois de cancelar: lista de pedidos