Dia 7 — useEffect com dado de fora

Informatica · Conteudo · publicado em 05/10/2026
Dia 7 de 16

useEffect com dado de fora

Aula 1

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
Aula 2

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