Dia 13 — useEffect e o primeiro app
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ências | Quando 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 array | a 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.logpara 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
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:
- o
Appnão está registrado, ou o nome dopackage.jsonnão bate com o do
registerComponent;
- 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;
- 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/