Dia 13 — useEffect e o primeiro app

Informatica · Conteudo · publicado em 05/10/2026
Dia 13 de 13

useEffect e o primeiro app

Aula 1

useEffect: o que roda depois da tela

useEffect: o que roda depois da tela

O corpo do componente serve para calcular o que vai ser desenhado. Tudo que

precisa acontecer depois que a tela existe — pedir dados, salvar no

armazenamento, iniciar um relógio, cadastrar um ouvinte — é efeito colateral,

e isso vai no useEffect.

const { useEffect } = require('react-native');

function useEffect(efeito, dependencias) {
  efeito();
}

O nome é o do contrato: a função que ele recebe é o efeito. O segundo

argumento é o array de dependências, e é ele que decide quando o efeito

roda:

DependênciasQuando o efeito roda
[]uma vez só, ao montar o componente
[a, b]ao montar, e de novo quando a ou b mudarem de valor
sem o arraya cada render

O executar uma vez é o []: o padrão do app é [] para o que é uma vez e a lista de dependências para o que

precisa reagir a um dado.

O exemplo avisa na primeira linha de código que não existe renderizador aqui, e

isso muda o que aparece no terminal: o efeito roda na hora da chamada e a

limpeza é chamada junto, em vez de esperar o componente sair da tela. As linhas

chamando o componente:, efeito com []: rodou uma vez e

limpeza: chamada ao desmontar, e aqui no fim da chamada são essa ordem.

O que o exemplo demonstra é o contrato, não a ordem: o [] significa uma vez. No

app, o mesmo efeito roda uma vez ao entrar na tela e a limpeza roda só ao sair —

que é o montar e desmontar que a seção da limpeza descreve.

O segundo efeito usa [total] e a linha

efeito com [total] rodou com total = 5 sai logo depois de

o valor da dependencia e: 5. As duas linhas juntas mostram a dependência: o

efeito leu exatamente o valor que estava na lista. O terceiro efeito não tem

array nenhum, e imprime efeito sem array, render numero 1 — o contador de

renders do próprio exemplo, que chegou em 1 porque o componente foi chamado

uma vez.

Por que não no corpo do componente

Porque o corpo roda a cada render. Se o pedido à API estivesse no corpo, ele

dispararia toda vez que qualquer estado mudasse — inclusive o estado que a

resposta dele mesmo preenche. O efeito com [] roda uma vez, e é por isso que o

pedido fica dentro dele.

function ListaDeTarefas() {
  const [tarefas, setTarefas] = useState([]);

  useEffect(() => {
    buscarTarefas().then(setTarefas);   // roda uma vez
  }, []);

  return <View />;
}

O componente ListaDeTarefas do exemplo é esse mesmo, e a linha

app: 0 tarefas mostra o estado inicial: a lista começou vazia e o pedido

ainda não respondeu. A linha `buscarTarefas().then(setTarefas) rodaria uma vez

aqui está dentro do efeito com []` — e a palavra rodaria é o ponto: o

exemplo não tem API, então imprime o que a chamada faria.

O parágrafo avisa sobre o laço. Se o pedido estivesse no corpo, ele dispararia

de novo quando o setTarefas preenchesse a lista — e o estado que a resposta

preenche é exatamente o estado que dispararia o pedido de novo. Com [], o

pedido sai uma vez e o estado muda sem novo pedido.

A limpeza

O return de cleanup é o return da função do efeito. O useEffect pode devolver uma função, e essa função é a limpeza (cleanup).

Ela roda antes do efeito de novo e quando o componente sai da tela — o desmontar.

useEffect(() => {
  const intervalo = setInterval(() => tick(), 1000);
  return () => clearInterval(intervalo);   // desmontou: para o relógio
}, []);

É a limpeza que evita o vazamento: intervalo rodando, ouvinte registrado e

requisição vieja chegando depois que a tela já saiu. Sem ela, trocar de tela dez

vezes deixa dez intervalos rodando.

O exemplo escreve o par de linhas do intervalo:

started: o intervalo foi criado e stopped: o intervalo foi cleared. São duas

linhas com a mesma indentação de dois espaços, e a segunda só existe porque

o efeito devolveu uma função.

O parágrafo é explícito sobre isso, e o exemplo confirma em uma linha:

retorno do efeito sem cleanup: undefined e

retorno do efeito com cleanup: function. Um efeito que só imprime não devolve

nada, e o typeof do retorno é undefined; o que devolve função tem

typeof function. É o typeof que diz ao React Native que existe limpeza.

O par montar e desmontar é o ciclo de vida do componente, e é o que o

efeito resolve:

  • montar: uma vez, ao entrar na tela;
  • atualizar: quando uma dependência mudar de valor;
  • desmontar: a limpeza, ao sair.

O detalhe do intervalo é que ele existe mesmo sem a limpeza: o setInterval

cria o relógio de qualquer jeito. Sem o clearInterval, o relógio continua

rodando depois que a tela saiu — e o exemplo mostra o par criado e destruído

para que a ordem fique visível.

Dependência que muda dispara de novo

[total] é o caso do contador: quando total muda, o efeito roda outra vez. É

isso que faz a tela de busca na API atualizar sozinha quando o filtro muda:

useEffect(() => {
  const achados = filtrar(tarefas, total);
  setResultado(achados);
}, [total]);   // so roda quando total mudar

O erro mais comum aqui é a lista de dependências incompleta: [total] quando o

efeito também usa tarefas. O efeito roda com valor velho de tarefas e a tela

fica inconsistente. A regra é: **toda variável de fora usada no efeito entra na

lista**, exceto o set e o que é declarado dentro do próprio efeito.

O exemplo escreve exatamente o caso que a regra pega: o efeito usa tarefas e

total, e a lista de dependências tem só [total]. A linha

o efeito buscou e achou: [ 'estudar' ] mostra o resultado — a função

filtrar(lista, total) pegou a tarefa pendente e cortou pelo total.

O que a linha o filtro e o set sao calculo puro: fica no corpo, nao no efeito

diz é outra coisa, e é a da seção seguinte: o filtrar é cálculo puro, sem

nada de fora, então ele não precisa estar no efeito. Poderia ser chamado

direto no corpo do componente.

O que não vai no efeito

  • Cálculo que dá para fazer no JSX direto: vai no JSX, não no efeito.
  • console.log para conferir valor: no corpo, não no efeito — no efeito ele

dispara no momento errado.

  • Pedido que depende do estado atual: no efeito, com o estado na lista de

dependências.

O efeito é para o mundo de fora do componente. Se a operação não fala com nada de

fora — não pede dado, não salva, não registra ouvinte — provavelmente é cálculo,

e cálculo é do corpo.

A linha do exemplo sobre isso é a mais direta da seção: o filtro e o set são

cálculo puro, e ficam no corpo. A distinção prática é uma pergunta só — essa

operação alcança alguma coisa fora do componente? Se alcança, vai no efeito; se

não, vai no corpo.

useRef e useMemo ao lado

useRef guarda um valor mutável que não causa redesenho, e é o lugar do que

não é tela: identificador do componente, medidor, referência do elemento.

useMemo guarda o resultado de um cálculo caro e só refaz quando as dependências

mudam. É para lista grande ou cálculo caro, não para valor simples — memoizar o

trivial só gasta sem resolver.

As duas linhas do exemplo são useRef: render 1 e useMemo: 3 itens. O

useRef mostra o contador interno em 1: o ref.current foi somado dentro do

componente e o valor entrou no Text sem passar pelo useState — é uma

escrita direta, que não causa redesenho. O useMemo mostra o

[1, 2, 3].map((n) => n * 2) calculado uma vez e guardado, com o length

entrando na tela.

O detalhe do useRef que importa é que ele pode mudar sem redesenhar. Por

isso ele serve para o que a tela não lê — identificador, referência de elemento,

último valor — e não substitui o useState quando o valor aparece na tela.

Exemplo

// useEffect: o efeito roda depois da tela, e a lista de dependencias
// decide quantas vezes ele roda.
const { View, Text, useState, useEffect, useRef, useMemo } = require('react-native');

/* efeito com lista vazia: roda uma vez, ao montar.
   Neste ambiente nao ha renderizador, entao o efeito roda na hora da chamada
   e a limpeza e chamada junto: no app a ordem seria desenhar, efeito, desenhar
   de novo se algo mudar, e limpeza so ao sair da tela. */
function Relogio() {
  useEffect(() => {
    console.log('efeito com []: rodou uma vez');
    return () => console.log('limpeza: chamada ao desmontar, e aqui no fim da chamada');
  }, []);
  return <Text>relogio</Text>;
}
console.log('chamando o componente:');
Relogio();

/* efeito com dependencia: roda quando o valor muda */
const [total] = useState(5);
useEffect(() => {
  console.log('  efeito com [total] rodou com total =', total);
}, [total]);
console.log('o valor da dependencia e:', total);

/* efeito sem array: roda a cada render */
let renders = 0;
function SemArray() {
  renders = renders + 1;
  useEffect(() => {
    console.log('  efeito sem array, render numero', renders);
  });
  return <Text>x</Text>;
}
SemArray();

/* a limpeza e o que impede o intervalo de continuar rodando */
function ComIntervalo() {
  useEffect(() => {
    console.log('  started: o intervalo foi criado');
    return () => console.log('  stopped: o intervalo foi cleared');
  }, []);
  return <Text>com intervalo</Text>;
}
ComIntervalo();

/* efeito que so imprime devolve undefined; a limpeza so existe quando o
   efeito devolve uma funcao */
const soImprime = () => { console.log('  efeito sem cleanup'); };
const comCleanup = () => {
  console.log('  efeito com cleanup');
  return () => console.log('  a funcao devolvida e a limpeza');
};
console.log('retorno do efeito sem cleanup:', soImprime());
console.log('retorno do efeito com cleanup:', typeof comCleanup());

/* useRef: guarda valor que nao redesenha */
function ContadorInterno() {
  const ref = useRef(0);
  ref.current = ref.current + 1;
  return <Text>{`render ${ref.current}`}</Text>;
}
console.log('useRef:', ContadorInterno().props.children);

/* useMemo: so refaz quando a dependencia muda */
function Caro() {
  const memo = useMemo(() => [1, 2, 3].map((n) => n * 2));
  return <Text>{`${memo.length} itens`}</Text>;
}
console.log('useMemo:', Caro().props.children);

/* o que vai no efeito: falar com o mundo de fora */
const tarefas = [{ id: 1, texto: 'estudar', feita: false }];
function filtrar(lista, total) {
  return lista.filter((t) => !t.feita).slice(0, total);
}
useEffect(() => {
  const achados = filtrar(tarefas, total);
  console.log('  o efeito buscou e achou:', achados.map((t) => t.texto));
}, [total]);
console.log('o filtro e o set sao calculo puro: fica no corpo, nao no efeito');

/* o componente que junta estado e efeito */
function ListaDeTarefas() {
  const [itens] = useState([]);
  useEffect(() => {
    console.log('  buscarTarefas().then(setTarefas) rodaria uma vez aqui');
  }, []);
  return <View><Text>{`${itens.length} tarefas`}</Text></View>;
}
console.log('app:', ListaDeTarefas().props.children.props.children);

Saída real

chamando o componente:
efeito com []: rodou uma vez
limpeza: chamada ao desmontar, e aqui no fim da chamada
  efeito com [total] rodou com total = 5
o valor da dependencia e: 5
  efeito sem array, render numero 1
  started: o intervalo foi criado
  stopped: o intervalo foi cleared
  efeito sem cleanup
retorno do efeito sem cleanup: undefined
  efeito com cleanup
retorno do efeito com cleanup: function
useRef: render 1
useMemo: 3 itens
  o efeito buscou e achou: [ 'estudar' ]
o filtro e o set sao calculo puro: fica no corpo, nao no efeito
  buscarTarefas().then(setTarefas) rodaria uma vez aqui
app: 0 tarefas
Aula 2

Montar o app e rodar a primeira versão

Montar o app e rodar a primeira versão

O app é um arquivo que declara o componente principal e o registra. É o ponto

onde a árvore de componentes começa, e o lugar que o Metro procura quando o

aplicativo sobe.

O registro do componente é o que diz ao React Native qual desenhar. O

ponto de entrada é o arquivo do componente principal, e ele é o

método que diz ao React Native qual componente desenhar:

import App from './App';

AppRegistry.registerComponent('nomeDoApp', () => App);

App é o nome que o esqueleto do projeto já traz, e App.js é o arquivo onde ele

vive. registerComponent recebe o nome do aplicativo e uma função que devolve o

componente — a função é chamada quando a tela precisa ser desenhada.

O exemplo faz a conferência do nome do registro e do package.json numa linha:

nome do registro: nomeDoApp | package.json: nomeDoApp | batem: true. Os dois

primeiros campos são o que cada arquivo tem, e o terceiro é a comparação — é o

true que garante que o aplicativo não abre em branco por causa do nome.

O package.json é o outro arquivo que importa, e o campo name dele é o que

tem de ser igual ao primeiro argumento do registerComponent. Nome diferente é

o erro que abre o aplicativo em branco, e a mensagem do erro não fala de nome

de arquivo — o que deixa a causa longe de onde o aluno olha.

A linha nome errado: outroNome | bate: false é a mesma comparação com o valor

errado, e o false é a causa número 1 da lista de tela em branco. Nome

diferente não dá erro de sintaxe nem mensagem óbvia: o aplicativo simplesmente

sobe e não desenha nada.

export default: o que sai do arquivo

export default marca o que o arquivo exporta, e o import do outro arquivo

guarda. É o par que liga os arquivos:

// App.js
export default function App() { }

// em outro arquivo
import App from './App';

Sem o export default, o import não acha o que trazer, e o erro é

undefined is not a function no ponto em que o componente é usado — longe da

causa.

O exemplo não tem dois arquivos, e ele diz isso no comentário do começo: usa

require porque o Node não lê import. No lugar da importação ele escreve

const importado = App, e a linha

o import traz o mesmo componente: true | nome: App é a prova de que o

import entrega a mesma função — e o nome: App mostra que o nome sobreviveu

pela importação.

O nome sobreviver é o que permite o AppRegistry encontrar o componente: o

registro passa uma função, e o React Native chama essa função. O que ele

desenha é o que ela devolve.

A estrutura do projeto

O esqueleto criado pelo projeto tem esta forma, e vale saber o nome de cada peça:

nomeDoApp/
├── App.js          componente principal
├── app.json        nome e ícone do aplicativo
├── package.json    dependências e scripts
├── index.js        ponto de entrada que registra o App
└── src/            o código que o aluno escreve

app.json e package.json são configuração, não código: nome do aplicativo,

ícone, orientação da tela, versão. O aluno passa a mexer neles no momento em que

o app precisa de nome e ícone próprios — antes disso eles podem ficar como

vieram.

A última linha do exemplo é a lista da estrutura, impressa como texto:

estrutura lida do projeto: App.js, app.json, package.json, index.js, src/. São

os cinco nomes do bloco acima, na ordem em que o projeto os usa. O src/ é o

único diretório, e é o único que o aluno cria dentro.

A primeira versão que roda

O componente principal é uma função que devolve tela, e ele usa os três

conceitos do trimestre: componente, estado e efeito.

function App() {
  const [total, setTotal] = useState(0);
  const [tarefas, setTarefas] = useState([]);

  useEffect(() => {
    // uma vez ao montar: é onde o pedido de dados entra
  }, []);

  return <View style={styles.container} />;
}

Rodar o app no simulador é o passo depois do registro. O primeiro app é o

App.js com registro feito; o app que roda em branco tem uma de três causas, e nessa ordem:

  1. o App não está registrado, ou o nome do package.json não bate com o do

registerComponent;

  1. o componente devolveu algo que não é JSX — App() precisa devolver o

elemento, e um console.log dentro do componente não altera o retorno;

  1. o erro está em um componente filho, e o nome do arquivo do erro aparece no

console.

O exemplo monta o App completo e mostra as três causas, uma linha para cada.

App montou; tarefas: 0 é o efeito com [] rodando uma vez ao montar, com a

lista ainda vazia. `tipo da raiz: View | style: { flex: 1, padding: 16,

backgroundColor: '#ffffff' }` é o que o registro entrega: uma função devolvendo

o elemento com o estilo do StyleSheet. E

o que a tela mostra: 0 tarefas é o texto que aparece dentro dele — o total

indo do useState para o Text.

A causa 2 tem duas linhas: isto imprime, mas nao devolve tela nenhuma e

sem return, o componente devolve: undefined. O console.log executou — ele

aparece no terminal — e a função devolveu undefined. É o caso exato do

parágrafo: o log apareceu, e mesmo assim a tela fica em branco.

A causa 3 sai em

`App.js, linha do componente filho: TypeError - Cannot read properties of null

(reading 'nome'). O usuario era null e o .nome` foi lido assim mesmo; o

nome do arquivo está no começo da linha porque é o console do Metro que mostra

qual arquivo e qual linha deu erro.

O fast refresh faz o recarregamento ser rápido: ao salvar o App.js, o

Metro reconstrói o pacote e a tela aparece atualizada, sem recompilar o

aplicativo. Se o fast refresh não atualizar, o defeito é o próprio app — erro de

sintaxe impede a reconstrução, e o console mostra o arquivo e a linha.

A linha `este arquivo esta integro: nao ha sintaxe errada aqui: este arquivo

roda inteiro` é a conferência oposta: o arquivo de exemplo passa na checagem de

sintaxe e roda inteiro. Quando o fast refresh não atualiza, o primeiro teste é

esse — se o arquivo tem erro de sintaxe, o Metro nem consegue remontar o pacote.

Rodar no simulador

O app roda em três lugares, e o código é o mesmo nos três:

  • simulador do iOS, pelo Xcode;
  • emulador do Android, pelo Android Studio;
  • aparelho de verdade, pelo cabo ou pela rede.

Para o console.log aparecer, o monitor é o do Metro (desenvolvimento), o

logcat (Android) ou o console do Xcode (iOS). É o mesmo console.log do

componente que o aluno já testava no terminal.

O package.json guarda também o script que inicia o Metro, e é o comando que

sobe a ponte entre o código e o aparelho:

npm start

A linha do exemplo é scripts: {"start":"react-native start"} — o nome do script

é start e o comando que ele roda é react-native start, que é o Metro. É o

mesmo npm start do bloco acima, lido do objeto.

O que o Metro faz não é compilar o aplicativo: é juntar os arquivos, resolver os

import e entregar o pacote para o motor JavaScript que roda dentro do

aparelho.

E é por isso que o App.js precisa do export default mesmo sem nenhum import

no projeto: o index.js é quem faz o import App from './App', e é ele que

passa a função para o AppRegistry. Sem o export, essa linha entrega

undefined, e o registro registra uma função que não devolve nada.

Exemplo

// Montar o app: o componente principal, o registro, a exportacao e as
// tres causas de tela em branco.
//
// Nota sobre o import: o exemplo usa `require` e `module.exports` porque o
// Node puro nao le `import`/`export`. No app os dois arquivos usam
// `import App from './App'` e `export default`, que e a mesma coisa.
const { View, Text, StyleSheet, useState, useEffect } = require('react-native');

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, backgroundColor: '#ffffff' },
});

/* o componente principal: uma funcao que devolve tela */
function App() {
  const [total, setTotal] = useState(0);
  const [tarefas, setTarefas] = useState([]);

  // uma vez ao montar: e onde o pedido de dados entra
  useEffect(() => {
    console.log('App montou; tarefas:', tarefas.length);
  }, []);

  return (
    <View style={styles.container}>
      <Text>{`${total} tarefas`}</Text>
    </View>
  );
}

/* o que o registro entrega ao React Native: o componente, pronto para desenhar */
const arvore = App();
console.log('tipo da raiz:', arvore.type, '| style:', arvore.props.style);
console.log('o que a tela mostra:', arvore.props.children.props.children);

/* export default: o que o arquivo exporta.
   No app o par e `export default App` e `import App from './App'`.
   Aqui o exemplo fica em um arquivo so, entao a importacao e simulada: */
const importado = App;
console.log('o import traz o mesmo componente:', importado === App, '| nome:', App.name);

/* o nome do registro tem de bater com o package.json */
const packageJson = { name: 'nomeDoApp' };
const nomeRegistrado = 'nomeDoApp';
console.log('nome do registro:', nomeRegistrado, '| package.json:', packageJson.name,
  '| batem:', nomeRegistrado === packageJson.name);

/* a causa 2: componente que devolveu algo que nao e JSX */
function DevolveLog() {
  console.log('isto imprime, mas nao devolve tela nenhuma');
}
console.log('sem return, o componente devolve:', DevolveLog());

/* a causa 1: nome diferente entre registro e package.json */
const nomeErrado = 'outroNome';
console.log('nome errado:', nomeErrado, '| bate:', nomeErrado === packageJson.name);

/* a causa 3: o erro vem de um filho, e o nome do arquivo vem do log */
try {
  const usuario = null;
  console.log(usuario.nome);
} catch (erro) {
  console.log('App.js, linha do componente filho:', erro.constructor.name, '-', erro.message);
}

/* o fast refresh nao atualiza quando o pacote nao reconstroi: um erro de
   sintaxe no arquivo impede o Metro de remontar o pacote. O sinal e o console
   mostrar o arquivo e a linha, e nada mudar na tela. */
const erroDeSintaxe = 'nao ha sintaxe errada aqui: este arquivo roda inteiro';
console.log('este arquivo esta integro:', erroDeSintaxe);

/* o script do package.json que sobe a ponte com o aparelho */
const packageJsonCompleto = {
  name: 'nomeDoApp',
  scripts: { start: 'react-native start' },
};
console.log('scripts:', JSON.stringify(packageJsonCompleto.scripts));
console.log('estrutura lida do projeto: App.js, app.json, package.json, index.js, src/');

Saída real

App montou; tarefas: 0
tipo da raiz: View | style: { flex: 1, padding: 16, backgroundColor: '#ffffff' }
o que a tela mostra: 0 tarefas
o import traz o mesmo componente: true | nome: App
nome do registro: nomeDoApp | package.json: nomeDoApp | batem: true
isto imprime, mas nao devolve tela nenhuma
sem return, o componente devolve: undefined
nome errado: outroNome | bate: false
App.js, linha do componente filho: TypeError - Cannot read properties of null (reading 'nome')
este arquivo esta integro: nao ha sintaxe errada aqui: este arquivo roda inteiro
scripts: {"start":"react-native start"}
estrutura lida do projeto: App.js, app.json, package.json, index.js, src/