Preserving and Resetting State
O estado é isolado entre componentes. O React acompanha qual estado pertence a qual componente com base em sua posição na árvore de UI. Você pode controlar quando preservar o estado e quando redefini-lo entre as renderizações.
Você aprenderá
- Quando o React escolhe preservar ou redefinir o estado
- Como forçar o React a redefinir o estado de um componente
- Como as chaves e os tipos afetam se o estado é preservado
O estado está vinculado a uma posição na árvore de renderização
O React constrói árvores de renderização para a estrutura de componentes em sua UI.
Quando você dá estado a um componente, pode pensar que o estado “vive” dentro do componente. Mas o estado é, na verdade, mantido dentro do React. O React associa cada pedaço de estado que está mantendo ao componente correto pela sua posição na árvore de renderização.
Aqui, há apenas uma tag JSX <Counter />, mas ela é renderizada em duas posições diferentes:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Veja como isso se parece em uma árvore:


Árvore do React
Estes são dois contadores separados porque cada um é renderizado em sua própria posição na árvore. Você normalmente não precisa pensar nessas posições para usar o React, mas pode ser útil entender como funciona.
No React, cada componente na tela tem um estado totalmente isolado. Por exemplo, se você renderizar dois componentes Counter lado a lado, cada um deles receberá seu próprio estado score e hover independente.
Tente clicar em ambos os contadores e observe que eles não afetam um ao outro:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Como você pode ver, quando um contador é atualizado, apenas o estado desse componente é atualizado:


Atualizando o estado
O React manterá o estado ativo enquanto você renderizar o mesmo componente na mesma posição na árvore. Para ver isso, incremente ambos os contadores, depois remova o segundo componente desmarcando a caixa de seleção “Renderizar o segundo contador” e, em seguida, adicione-o de volta marcando-a novamente:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Note como no momento em que você para de renderizar o segundo contador, seu estado desaparece completamente. Isso ocorre porque quando o React remove um componente, ele destrói seu estado.


Excluindo um componente
Quando você marca “Renderizar o segundo contador”, um segundo Counter e seu estado são inicializados do zero (score = 0) e adicionados ao DOM.


Adicionando um componente
O React preserva o estado de um componente enquanto ele estiver sendo renderizado em sua posição na árvore de UI. Se ele for removido, ou um componente diferente for renderizado na mesma posição, o React descartará seu estado.
O mesmo componente na mesma posição preserva o estado
Neste exemplo, existem duas tags <Counter /> diferentes:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Quando você marca ou desmarca a caixa de seleção, o estado do contador não é redefinido. Se isFancy for true ou false, você sempre terá um <Counter /> como o primeiro filho do div retornado do componente raiz App:


Atualizar o estado do App não redefine o Counter porque o Counter permanece na mesma posição
É o mesmo componente na mesma posição, então, da perspectiva do React, é o mesmo contador.
Componentes diferentes na mesma posição redefinem o estado
Neste exemplo, marcar a caixa de seleção substituirá <Counter> por um <p>:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Aqui, você alterna entre diferentes tipos de componentes na mesma posição. Inicialmente, o primeiro filho do <div> continha um Counter. Mas quando você o substituiu por um p, o React removeu o Counter da árvore de UI e destruiu seu estado.


Quando Counter muda para p, o Counter é deletado e o p é adicionado


Ao voltar, o p é deletado e o Counter é adicionado
Além disso, quando você renderiza um componente diferente na mesma posição, ele redefine o estado de toda a sua subárvore. Para ver como isso funciona, incremente o contador e depois marque a caixa de seleção:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
O estado do contador é redefinido quando você clica na caixa de seleção. Embora você renderize um Counter, o primeiro filho do div muda de uma section para um div. Quando a section filha foi removida do DOM, toda a árvore abaixo dela (incluindo o Counter e seu estado) também foi destruída.


Quando section muda para div, a section é deletada e o novo div é adicionado


Ao alternar de volta, o div é deletado e a nova section é adicionada
Como regra geral, se você quiser preservar o estado entre as re-renderizações, a estrutura da sua árvore precisa “corresponder” de uma renderização para outra. Se a estrutura for diferente, o estado é destruído porque o React destrói o estado quando remove um componente da árvore.
Redefinindo o estado na mesma posição
Por padrão, o React preserva o estado de um componente enquanto ele permanece na mesma posição. Geralmente, é exatamente isso que você deseja, então faz sentido como comportamento padrão. Mas, às vezes, você pode querer redefinir o estado de um componente. Considere este aplicativo que permite que dois jogadores acompanhem suas pontuações a cada turno:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Próximo jogador! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Pontuação de {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Adicionar um </button> </div> ); }
Atualmente, quando você troca de jogador, a pontuação é preservada. Os dois Counters aparecem na mesma posição, então o React os considera o mesmo Counter cuja prop person foi alterada.
Mas, conceitualmente, neste aplicativo, eles deveriam ser dois contadores separados. Eles podem aparecer no mesmo lugar na interface do usuário, mas um é um contador para Taylor e outro é um contador para Sarah.
Existem duas maneiras de redefinir o estado ao alternar entre eles:
- Renderizar componentes em posições diferentes
- Dar a cada componente uma identidade explícita com
key
Opção 1: Renderizando um componente em posições diferentes
Se você deseja que esses dois Counters sejam independentes, pode renderizá-los em duas posições diferentes:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Próximo jogador! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Pontuação de {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Adicionar um </button> </div> ); }
- Inicialmente,
isPlayerAétrue. Portanto, a primeira posição contém o estado doCounter, e a segunda está vazia. - Quando você clica no botão “Próximo jogador”, a primeira posição é limpa, mas a segunda agora contém um
Counter.


Estado inicial


Clicando em “próximo”


Clicando em “próximo” novamente
O estado de cada Counter é destruído toda vez que ele é removido do DOM. É por isso que eles são redefinidos toda vez que você clica no botão.
Esta solução é conveniente quando você tem apenas alguns componentes independentes renderizados no mesmo lugar. Neste exemplo, você tem apenas dois, então não é um incômodo renderizá-los separadamente no JSX.
Opção 2: Redefinindo o estado com uma chave
Há também outra maneira, mais genérica, de redefinir o estado de um componente.
Você pode ter visto keys ao renderizar listas. As chaves não servem apenas para listas! Você pode usar chaves para fazer o React distinguir entre quaisquer componentes. Por padrão, o React usa a ordem dentro do pai (“primeiro contador”, “segundo contador”) para discernir entre componentes. Mas as chaves permitem que você diga ao React que este não é apenas um primeiro contador ou um segundo contador, mas um contador específico - por exemplo, o contador de Taylor. Dessa forma, o React saberá o contador de Taylor onde quer que ele apareça na árvore!
Neste exemplo, os dois <Counter />s não compartilham estado, mesmo que apareçam no mesmo lugar no JSX:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Próximo jogador! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Pontuação de {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Adicionar um </button> </div> ); }
Alternar entre Taylor e Sarah não preserva o estado. Isso ocorre porque você deu a eles keys diferentes:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}Especificar uma key diz ao React para usar a key em si como parte da posição, em vez de sua ordem dentro do pai. É por isso que, mesmo que você os renderize no mesmo lugar no JSX, o React os vê como dois contadores diferentes, e assim eles nunca compartilharão estado. Toda vez que um contador aparece na tela, seu estado é criado. Toda vez que ele é removido, seu estado é destruído. Alternar entre eles redefine seu estado repetidamente.
Redefinindo um formulário com uma chave
Redefinir o estado com uma chave é particularmente útil ao lidar com formulários.
Neste aplicativo de chat, o componente <Chat> contém o estado da entrada de texto:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Tente digitar algo na entrada e, em seguida, clique em “Alice” ou “Bob” para escolher um destinatário diferente. Você notará que o estado da entrada é preservado porque o <Chat> é renderizado na mesma posição na árvore.
Em muitos aplicativos, este pode ser o comportamento desejado, mas não em um aplicativo de chat! Você não quer que o usuário envie uma mensagem que já digitou para a pessoa errada devido a um clique acidental. Para corrigir isso, adicione uma key:
<Chat key={to.id} contact={to} />Isso garante que, quando você selecionar um destinatário diferente, o componente <Chat> será recriado do zero, incluindo qualquer estado na árvore abaixo dele. O React também recriará os elementos DOM em vez de reutilizá-los.
Agora, alternar o destinatário sempre limpa o campo de texto:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Deep Dive
Em um aplicativo de chat real, você provavelmente gostaria de recuperar o estado da entrada quando o usuário selecionar o destinatário anterior novamente. Existem algumas maneiras de manter o estado “vivo” para um componente que não está mais visível:
- Você poderia renderizar todos os chats em vez de apenas o atual, mas ocultar todos os outros com CSS. Os chats não seriam removidos da árvore, então seu estado local seria preservado. Essa solução funciona bem para interfaces simples. Mas pode ficar muito lenta se as árvores ocultas forem grandes e contiverem muitos nós DOM.
- Você poderia elevar o estado e manter a mensagem pendente para cada destinatário no componente pai. Dessa forma, quando os componentes filhos forem removidos, não importa, porque é o pai que mantém as informações importantes. Esta é a solução mais comum.
- Você também pode usar uma fonte diferente além do estado do React. Por exemplo, você provavelmente quer que um rascunho de mensagem persista mesmo que o usuário feche acidentalmente a página. Para implementar isso, você pode fazer o componente
<Chat>inicializar seu estado lendo dolocalStoragee salvar os rascunhos lá também.
Não importa qual estratégia você escolha, um chat com Alice é conceitualmente distinto de um chat com Bob, então faz sentido dar uma key à árvore <Chat> com base no destinatário atual.
Recap
- O React mantém o estado enquanto o mesmo componente for renderizado na mesma posição.
- O estado não é mantido em tags JSX. Ele está associado à posição na árvore onde você colocou esse JSX.
- Você pode forçar uma subárvore a redefinir seu estado dando a ela uma chave diferente.
- Não aninhe definições de componentes, ou você redefinirá o estado acidentalmente.
Challenge 1 of 5: Corrigir o texto de entrada que desaparece
Este exemplo mostra uma mensagem quando você pressiona o botão. No entanto, pressionar o botão também redefine acidentalmente a entrada. Por que isso acontece? Corrija para que pressionar o botão não redefina o texto da entrada.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }