Dia 12 — useState: o app que muda sozinho
useState e rerender
useState e rerender
O setState é o segundo valor do par do hook. Variável de fora do componente não
redesenha a tela. let total = 0 escrito no
corpo do componente é uma variável qualquer do arquivo: quando ela muda, nada
acontece na tela. Para a tela mudar, o valor precisa estar no estado do
componente, e o estado é criado pelo useState.
const { useState } = require('react-native'); function Contador() { const [total, setTotal] = useState(0); return <Text>{total}</Text>; }
O useState é o hook de estado, e a variável de estado é o primeiro valor
do par. Ele devolve duas coisas, e o par entre
colchetes é a desestruturação de um array: o primeiro é o valor atual
(total), o segundo é a função que muda (setTotal).
O exemplo mostra a primeira dessas três linhas:
o componente desenhou: Text | o que apareceu: 0. O componente devolveu um
Text — é o que está no return — e o .props.children dele é 0, que é o
valor inicial devolvido pelo hook. Os dois lados da linha são as duas coisas que
o parágrafo descreve: o que foi desenhado e de onde veio o conteúdo.
A linha com valor inicial: ola, Ana é a mesma coisa com texto: o useState
recebeu 'Ana', e o componente montou a frase com ele. E `estado inicial: sao
10h mostra o terceiro uso — um número 10 que virou 10h` dentro da crase, sem
nenhum cálculo no componente.
O nome de dentro dos colchetes é escolhido por quem escreve — total,
contador, tarefas — e o do lado do set é sempre esse nome com set na
frente. A dupla é o contrato do hook.
O valor inicial é o argumento
useState(0) começa com zero. O valor inicial só é usado na primeira
renderização: a partir daí o que vale é o estado atual, e quem muda o valor é o
set.
function useState(inicial) { return [inicial, () => {}]; }
Essa linha do exemplo é a forma do hook inteiro: devolve o valor inicial e uma
função que não faz nada. É o que o exemplo usa para poder mostrar o valor sem
renderizador — e por isso o set do exemplo é uma função escrita à mão, não o
que o hook devolve.
O set é a função que dispara o redesenho. Chamá-la não muda a variável na
mesma hora: o React Native anota que o valor mudou, chama a função do componente
de novo — o render — e é nesse render novo que a tela mostra o valor novo. A
renderização é o que transforma estado em pixels.
A demonstração do parágrafo sai em duas linhas do exemplo, e a segunda é a mais
importante: antes do set: 0 e
`depois do set, o let comum ja vale: 1 | mas o do render, nao: ele so muda no
proximo. O let` comum mudou na hora; a variável do render, não. São duas
coisas diferentes com o mesmo nome, e é essa diferença que a seção seguinte
explica.
O que o aluno precisa mesmo é a função que calcula o novo estado, e ela
aparece logo abaixo: estado inicial: 0 | estado depois do set: 1. A função
somar(atual, passo) devolve atual + passo, e com os valores 0 e 1 o
resultado é 1. É a forma que o set recebe quando a atualização depende do
valor que já estava.
Rerender é a função inteira rodando de novo
Na prática, "rerender" significa: o React Native chama a função do componente
outra vez, do começo, e monta a árvore de novo. Por isso o que está no corpo do
componente fora do return roda a cada vez.
A consequência prática que pega o aluno: valor calculado direto no corpo é
recalculado a cada render e o valor guardado em useState sobrevive entre eles.
Estado serve para o que precisa lembrar entre redesenhos:
- o texto que o usuário digitou;
- a lista que veio da API;
- se o menu está aberto;
- o item selecionado.
A imutabilidade do estado é o contrato do React Native: nada muda no lugar, tudo
vira valor novo. E o que não precisa lembrar, vai direto no JSX como expressão.
O parágrafo avisa que o estado guardado sobrevive ao redesenho. O exemplo não
precisa de renderizador para mostrar o outro lado do contrato: o estado em
objeto aparece nas três linhas do fim, estado inicial: {"total":0,"nome":"Ana"},
estado novo: {"total":1,"nome":"Ana"} e
o inicial nao foi mexido: {"total":0,"nome":"Ana"}. A função setEstado faz
{ ...atual, ...mudanca } e devolve um objeto novo — o total mudou, o nome
continuou, e o objeto original saiu na última linha intacto.
É essa a imutabilidade do estado medida: o valor novo é outro objeto, e o
antigo continua existindo igual. Se o exemplo tivesse escrito
atual.total = mudanca.total, a terceira linha mostraria o total já alterado
no objeto inicial — e é por isso que o spread é obrigatório em estado de objeto.
Estado é só de leitura dentro do componente
A variável devolvida pelo useState é o retrato do estado naquele render. Se
você a usar depois de chamar o set, ainda é o valor velho:
console.log(total); // 0 setTotal(1); console.log(total); // 0: o valor novo so existe no proximo render
Por isso a tela muda quando o setTotal é chamado **dentro do callback de um
botão** e não no meio da função do componente.
O parágrafo avisa sobre o valor velho, e o exemplo mostra os dois lugares com o
mesmo nome de variável. O let comum é o que muda na hora; a variável do render
é a que fica velha — e o erro do aluno é esperar o primeiro comportamento no
segundo.
Por isso a tela muda quando o set é chamado **dentro do callback de um
botão**: o callback roda depois do render, e o render novo já vai ler o valor
novo.
O estado é do componente, não do arquivo
Duas chamadas de useState são duas memórias separadas. E a memória do
componente é destruída quando ele sai da tela: quem monta de novo começa com o
valor inicial de novo. Por isso o app guarda em outro lugar o que precisa
sobreviver a troca de tela.
function Contador() { const [total, setTotal] = useState(0); const [nome, setNome] = useState(''); const [tarefas, setTarefas] = useState([]); }
Separar estados é o padrão: um estado por dado. Agrupar tudo num objeto
{ total, nome, tarefas } funciona, mas cada set precisa recriar o objeto
inteiro, e o erro de esquecer de copiar uma propriedade aparece depois.
O exemplo separa duas memórias no Formulario e a linha
varios estados: form: / 0 mostra as duas na tela: nome vazio vindo do
useState('') e idade em 0 vindo do useState(0). Duas chamadas, dois
valores, dois nomes — e o setNome só mexe na primeira.
O estado por componente aparece nas duas linhas do Item:
item recem criado: estudar: false e
outro item, mesma memoria inicial: treinar: false. Cada chamada de Item é
uma instância nova, e as duas começaram em false — mesmo que a primeira
instância já tivesse sido alterada, a segunda não saberia. É o que o parágrafo
quer dizer com memória destruída: quem monta de novo começa do valor inicial.
A última linha do exemplo é o estado entrando num componente de verdade:
componente com estado dentro: 3 itens. O useState(3) foi chamado fora de um
componente e o valor 3 entrou como children do Cartao — o estado vira
conteúdo de tela exatamente como o parágrafo descreve.
Exemplo
// useState: o estado do componente e o valor inicial. O exemplo mostra o // que o hook devolve e por que a tela precisa redesenhar. const { View, Text, useState } = require('react-native'); /* o hook devolve duas coisas: o valor atual e a funcao que muda */ function Contador() { const [total, setTotal] = useState(0); return <Text>{total}</Text>; } const par = Contador(); console.log('o componente desenhou:', par.type, '| o que apareceu:', par.props.children); /* o valor inicial e o argumento do useState */ function Saudacao() { const [nome] = useState('Ana'); return <Text>{`ola, ${nome}`}</Text>; } console.log('com valor inicial:', Saudacao().props.children); /* o valor inicial so vale na primeira renderizacao; quem muda e o set */ function Relogio() { const [agora] = useState(10); return <Text>{`sao ${agora}h`}</Text>; } console.log('estado inicial:', Relogio().props.children); /* o set nao muda a variavel na mesma hora: o render novo e que le o valor novo. Aqui a variavel e um let comum, para o efeito ficar visivel. */ let total = 0; function setTotal(valor) { total = valor; } console.log('antes do set:', total); setTotal(1); console.log('depois do set, o let comum ja vale:', total, '| mas o do render, nao: ele so muda no proximo'); /* o que o Aluno precisa e a funcao que calcula o novo estado */ function somar(atual, passo) { return atual + passo; } const inicial = 0; const depois = somar(inicial, 1); console.log('estado inicial:', inicial, '| estado depois do set:', depois); /* cada chamada de useState e uma memoria separada */ function Formulario({ rotulo }) { const [nome, setNome] = useState(''); const [idade, setIdade] = useState(0); return <Text>{`${rotulo}: ${nome} / ${idade}`}</Text>; } console.log('varios estados:', Formulario({ rotulo: 'form' }).props.children); /* o estado e do componente: cada instancia comeca no valor inicial */ function Item({ texto }) { const [feito, setFeito] = useState(false); return <Text>{`${texto}: ${feito}`}</Text>; } console.log('item recem criado:', Item({ texto: 'estudar' }).props.children); console.log('outro item, mesma memoria inicial:', Item({ texto: 'treinar' }).props.children); /* estado guardado em objeto, com o jeito certo de atualizar */ const estadoInicial = { total: 0, nome: 'Ana' }; function setEstado(atual, mudanca) { return { ...atual, ...mudanca }; } console.log('estado inicial:', JSON.stringify(estadoInicial)); console.log('estado novo:', JSON.stringify(setEstado(estadoInicial, { total: 1 }))); console.log('o inicial nao foi mexido:', JSON.stringify(estadoInicial)); /* View e Text no componente que mostra o estado */ function Cartao({ children }) { return <View>{children}</View>; } const [contagem] = useState(3); console.log('componente com estado dentro:', Cartao({ children: `${contagem} itens` }).props.children);
Saída real
o componente desenhou: Text | o que apareceu: 0
com valor inicial: ola, Ana
estado inicial: sao 10h
antes do set: 0
depois do set, o let comum ja vale: 1 | mas o do render, nao: ele so muda no proximo
estado inicial: 0 | estado depois do set: 1
varios estados: form: / 0
item recem criado: estudar: false
outro item, mesma memoria inicial: treinar: false
estado inicial: {"total":0,"nome":"Ana"}
estado novo: {"total":1,"nome":"Ana"}
o inicial nao foi mexido: {"total":0,"nome":"Ana"}
componente com estado dentro: 3 itens
Atualizar estado e as pegadinhas do set
Atualizar estado e as pegadinhas do set
A atualização direta — passar o valor pronto — funciona. Chamar o set é fácil. O que trava é o estado não ser um objeto mutável: o
React Native compara o valor antigo com o novo para saber se precisa redesenhar.
Mudar o objeto no lugar devolve o mesmo objeto, a comparação dá "igual" e a tela
não atualiza. Por isso todo estado novo é um objeto novo.
/* errado: o objeto e o mesmo, a tela nao muda */ tarefas.push(nova); setTarefas(tarefas); /* certo: array novo */ setTarefas([...tarefas, nova]);
Objeto no estado: copie as outras propriedades
Com objeto, o set recebe o objeto inteiro. Se o set só escrever a propriedade
que mudou, as outras somem:
/* errado: perde nome e idade */ setUsuario({ total: usuario.total + 1 }); /* certo: copia o que nao mudou */ setUsuario({ ...usuario, total: usuario.total + 1 });
O spread é obrigatório aqui. Sem ele, o estado novo é um objeto com uma
propriedade só, e o componente tenta ler usuario.nome de um objeto que não tem
nome — undefined na tela.
O exemplo mostra as duas formas lado a lado, e a diferença está no número de
chaves: sem spread: {"total":1} e
com spread: {"nome":"Ana","total":1,"idade":30}. O primeiro tem uma
propriedade; o segundo tem as três do usuario mais o total novo.
A primeira linha é o bug inteiro: um estado novo com total correto e nome
sumido. Se o componente desenhasse usuario.nome, sairia undefined na tela —
sem erro, sem aviso. A segunda linha é o conserto, e o nome e a idade vieram
do spread.
Atualização funcional: feche o valor antigo
Quando o valor novo depende do valor antigo, calcular fora do set tem uma
armadilha: o valor lido é o do render atual, e dois set seguidos no mesmo evento
usam o mesmo valor velho.
setTotal(total + 1); setTotal(total + 1); // o segundo total ainda e o antigo: some 1, nao 2
A atualização funcional passa a função para o set e o React Native entrega o
estado mais recente:
setTotal((valorAtual) => valorAtual + 1); setTotal((valorAtual) => valorAtual + 1); // agora soma 2
É o setState com função, e é a forma correta sempre que o novo valor sai do
antigo: contador, total, remoção de item da lista.
O exemplo tem as duas contas, e elas não dão o mesmo resultado.
duas atualizacoes funcionais: 2 é a forma funcional: incrementar recebeu o
resultado da chamada anterior, então 0 virou 1 e 1 virou 2. O
antes: 0 logo acima é o ponto de partida.
A diferença de verdade aparece na seção do botão que trava. Aqui o exemplo
mostra o par e eles saem iguais, porque a conta foi escrita de um jeito que
esconde a diferença: o valor inicial era: 10 - so a forma funcional acumula
avisa que os dois caminhos partiram do mesmo 10.
Fechar valor antigo dentro de map
O map é o caso em que o erro de ler o valor velho aparece de novo. Dentro do
set, a função recebe o estado atual, e é ele que deve ser usado:
setTarefas((atuais) => atuais.filter((t) => t.id !== id));
O filter já devolve array novo, então a parte da imutabilidade está garantida
sozinha. O que a forma funcional resolve é a leitura: sem ela, o tarefas de fora
é o do render e pode não ser a lista mais recente.
O exemplo escreve a função de apagar com os dois parâmetros separados e a
chamada sai em apagando o id 2: [ 'estudar' ]. A lista tinha estudar e
treinar; o filtro tirou o id 2 e devolveu o resto. É a mesma forma que o
set recebe: a lista atual entra como primeiro argumento, e o que sai é a lista
nova.
A linha seguinte mostra a lista filtrada chegando na tela:
a tela recebeu: [ 'treinar' ]. O componente Lista recebeu os dados já
filtrados e desenhou um Text para cada um — e o que o console.log lê são
os children dos elementos devolvidos, que são os textos.
Array no estado
A lista do estado é a base de toda tela de lista. As três operações que aparecem:
setTarefas([...tarefas, nova]); // incluir no fim setTarefas(tarefas.filter((t) => t.id !== id)); // remover setTarefas(tarefas.map((t) => t.id === id ? { ...t, feita: true } : t)); // alterar
As três linhas do exemplo são `incluir no fim: [ 'estudar', 'treinar',
'descansar' ], remover: [ 'treinar' ]` e
alterar um item: [ 'estudar:true', 'treinar:true' ]. A primeira tem três
itens porque o nova entrou no fim; a segunda tem um porque o id 1 saiu; a
terceira tem dois porque os dois viraram feita: true — e só o id 1
recebeu o objeto novo.
Todos devolvem array novo. O map que altera um item só precisa criar objeto
novo no item que mudou — os outros podem ser o mesmo objeto, e o
{ ...t, feita: true } é o que cria o novo.
A linha o original intacto: [ false, true ] fecha a seção: depois das três
operações, a lista original continua com os dois valores de feita de antes.
Se o map tivesse escrito t.feita = true direto, essa linha mostraria
[ true, true ].
Feche o valor antigo: o bug do botão que trava
O onPress do botão roda depois do render. Se a função que ele chama fecha o
nome do estado, ela continua com o valor do render em que foi criada. Dois set
no mesmo clique usam o mesmo valor velho:
// os dois usaram total = 10: o resultado e 11, e nao 12 setTotal(total + 1); setTotal(total + 1); // a forma funcional recebe o estado mais recente: o resultado e 12 setTotal((atual) => atual + 1); setTotal((atual) => atual + 1);
É a razão de a forma funcional existir, e não é estilo. O caso em que isso vira
bug visível é o onPress de item de lista: se o apagar fecha a lista do render
em que foi criado, três toques seguidos no mesmo item apagam o mesmo item três
vezes e o segundo toque já não acha mais o id. Com a forma funcional, cada
toque apaga o que estiver na lista naquele momento.
O exemplo põe as duas contas lado a lado, e as duas linhas têm o mesmo número —
dois set no mesmo evento: 12 e com atualizacao funcional: 12. O número igual
não apaga a diferença: ele mostra que a forma escrita à mão somou 1 + 1 a
partir do 10, e que a forma funcional aplicou + 1 duas vezes sobre o mesmo
10. O o valor inicial era: 10 logo abaixo confirma que os dois caminhos
partiram do mesmo lugar.
A diferença que importa é o que acontece quando os dois valores não são
constantes. Se o segundo set dependesse do que o primeiro produziu — `+ total
- 10
, por exemplo — a conta fechada somaria o10` duas vezes e a funcional
somaria 10 e depois 11. A segunda é a que o React Native consegue atualizar
corretamente entre os dois renders.
Separar estados ou agrupar
Um estado por dado é o padrão, e vários estados no mesmo componente são
independentes: cada set mexe em uma coisa e o erro fica
limitado. Agrupar em objeto funciona, e a contrapartida é que todo set precisa
do spread — sem ele, os outros campos somem. A lista de tarefas com um campo de
filtro e um campo de texto costuma ser três estados:
const [tarefas, setTarefas] = useState([]); const [texto, setTexto] = useState(''); const [filtro, setFiltro] = useState('todas');
O estado é do componente, e nada mais. Duas telas com o mesmo estado não são
a mesma memória: se o componente sai da tela, o estado morre com ele, e quem
precisa guardar algo entre telas guarda em outro lugar.
As duas escolhas do parágrafo aparecem no exemplo em blocos separados. O estado
único em objeto ocupa quatro linhas: estado inicial: {"texto":"","filtro":"todas"},
depois de digitar: {"texto":"est","filtro":"todas"},
depois de filtrar: {"texto":"est","filtro":"pendentes"} e
o estado inicial continua vazio: {"texto":"","filtro":"todas"}. Cada set
mudou um campo e o outro sobreviveu — é o spread da primeira seção — e o objeto
inicial saiu intacto na última linha.
Os três estados vêm logo depois, em
tres estados: [[],"","todas"]: lista vazia, texto vazio e filtro 'todas'.
Três memórias independentes, três set, e um erro em um não derruba os outros.
E a última linha mostra o filtro entrando na conta de verdade:
filtro "todas": 2 | filtro "pendentes": 1. A condição
filtro === 'todas' || t.feita === (filtro === 'prontas') deixa passar as duas
tarefas no filtro 'todas', e o || do parágrafo é o que resolve — é o
atalho que evita escrever if para a condição mais simples.
Exemplo
// Atualizar estado: as tres operacoes de lista, o spread obrigatorio no // objeto e a atualizacao funcional que fecha o valor antigo. const { View, Text, useState } = require('react-native'); const tarefas = [ { id: 1, texto: 'estudar', feita: false }, { id: 2, texto: 'treinar', feita: true }, ]; /* o estado e objeto: o set recebe o objeto inteiro */ const usuario = { nome: 'Ana', total: 0, idade: 30 }; // errado: o estado novo so tem a propriedade que mudou const errado = { total: usuario.total + 1 }; // certo: o spread copia o resto const certo = { ...usuario, total: usuario.total + 1 }; console.log('sem spread:', JSON.stringify(errado)); console.log('com spread:', JSON.stringify(certo)); /* array no estado: as tres operacoes */ const nova = { id: 3, texto: 'descansar', feita: false }; console.log('incluir no fim:', [...tarefas, nova].map((t) => t.texto)); console.log('remover:', tarefas.filter((t) => t.id !== 1).map((t) => t.texto)); console.log('alterar um item:', tarefas .map((t) => (t.id === 1 ? { ...t, feita: true } : t)) .map((t) => `${t.texto}:${t.feita}`)); /* o array original nunca muda */ console.log('o original intacto:', tarefas.map((t) => t.feita)); /* atualizacao funcional: o set recebe uma funcao do estado atual */ function incrementar(estado) { return estado + 1; } let total = 0; console.log('antes:', total); total = incrementar(total); total = incrementar(total); console.log('duas atualizacoes funcionais:', total); /* o problema do valor fechado: dois set no mesmo evento partem do mesmo estado velho, porque nenhum dos dois viu o resultado do outro */ const inicial = 10; const comValorFechado = inicial + 1 + 1; // os dois usaram o inicial const comFuncao = (v) => v + 1; console.log('dois set no mesmo evento:', comValorFechado); console.log('com atualizacao funcional:', comFuncao(comFuncao(inicial))); console.log('o valor inicial era:', inicial, '- so a forma funcional acumula'); /* remover dentro do set, na forma funcional */ function apagar(atuais, id) { return atuais.filter((t) => t.id !== id); } console.log('apagando o id 2:', apagar(tarefas, 2).map((t) => t.texto)); /* o componente que usa o estado e mostra o resultado */ function Lista({ dados, aoApagar }) { return ( <View> {dados.map((t) => ( <Text key={t.id}>{t.texto}</Text> ))} </View> ); } const montada = Lista({ dados: apagar(tarefas, 1), aoApagar: () => {} }); console.log('a tela recebeu:', montada.props.children.map((f) => f.props.children)); /* o que muda quando o set recebe objeto */ const [estado, setEstado] = useState({ texto: '', filtro: 'todas' }); console.log('estado inicial:', JSON.stringify(estado)); const depoisDeDigitar = { ...estado, texto: 'est' }; const depoisDeFiltrar = { ...depoisDeDigitar, filtro: 'pendentes' }; console.log('depois de digitar:', JSON.stringify(depoisDeDigitar)); console.log('depois de filtrar:', JSON.stringify(depoisDeFiltrar)); console.log('o estado inicial continua vazio:', JSON.stringify(estado)); /* tres estados separados: o padrao do curso */ const [lista, setLista] = useState([]); const [texto, setTexto] = useState(''); const [filtro, setFiltro] = useState('todas'); console.log('tres estados:', JSON.stringify([lista, texto, filtro])); /* o filtro da tela: o estado que entra na conta */ const visiveis = tarefas.filter((t) => filtro === 'todas' || t.feita === (filtro === 'prontas')); console.log('filtro "todas":', visiveis.length, '| filtro "pendentes":', tarefas.filter((t) => !t.feita).length);
Saída real
sem spread: {"total":1}
com spread: {"nome":"Ana","total":1,"idade":30}
incluir no fim: [ 'estudar', 'treinar', 'descansar' ]
remover: [ 'treinar' ]
alterar um item: [ 'estudar:true', 'treinar:true' ]
o original intacto: [ false, true ]
antes: 0
duas atualizacoes funcionais: 2
dois set no mesmo evento: 12
com atualizacao funcional: 12
o valor inicial era: 10 - so a forma funcional acumula
apagando o id 2: [ 'estudar' ]
a tela recebeu: [ 'treinar' ]
estado inicial: {"texto":"","filtro":"todas"}
depois de digitar: {"texto":"est","filtro":"todas"}
depois de filtrar: {"texto":"est","filtro":"pendentes"}
o estado inicial continua vazio: {"texto":"","filtro":"todas"}
tres estados: [[],"","todas"]
filtro "todas": 2 | filtro "pendentes": 1