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:

Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. Cada um dos filhos é rotulado 'Counter' e ambos contêm uma bolha de estado rotulada 'count' com valor 0.
Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. Cada um dos filhos é rotulado 'Counter' e ambos contêm uma bolha de estado rotulada 'count' com valor 0.

Á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:

Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 1. A bolha de estado do filho direito está destacada em amarelo para indicar que seu valor foi atualizado.
Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 1. A bolha de estado do filho direito está destacada em amarelo para indicar que seu valor foi 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.

Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito está faltando e em seu lugar há uma imagem de 'poof' amarela, destacando o componente sendo excluído da árvore.
Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito está faltando e em seu lugar há uma imagem de 'poof' amarela, destacando o componente sendo excluído da árvore.

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.

Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. Todo o nó filho direito está destacado em amarelo, indicando que ele acabou de ser adicionado à árvore.
Diagrama de uma árvore de componentes React. O nó raiz é rotulado 'div' e tem dois filhos. O filho esquerdo é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. O filho direito é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com valor 0. Todo o nó filho direito está destacado em amarelo, indicando que ele acabou de ser adicionado à árvore.

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:

Diagrama com duas seções separadas por uma seta fazendo a transição entre elas. Cada seção contém um layout de componentes com um pai rotulado 'App' contendo uma bolha de estado rotulada isFancy. Este componente tem um filho rotulado 'div', que leva a uma bolha de props contendo isFancy (destacada em roxo) passada para o único filho. O último filho é rotulado 'Counter' e contém uma bolha de estado com rótulo 'count' e valor 3 em ambos os diagramas. Na seção esquerda do diagrama, nada está destacado e o valor do estado pai isFancy é falso. Na seção direita do diagrama, o valor do estado pai isFancy mudou para true e está destacado em amarelo, assim como a bolha de props abaixo, que também mudou seu valor isFancy para true.
Diagrama com duas seções separadas por uma seta fazendo a transição entre elas. Cada seção contém um layout de componentes com um pai rotulado 'App' contendo uma bolha de estado rotulada isFancy. Este componente tem um filho rotulado 'div', que leva a uma bolha de props contendo isFancy (destacada em roxo) passada para o único filho. O último filho é rotulado 'Counter' e contém uma bolha de estado com rótulo 'count' e valor 3 em ambos os diagramas. Na seção esquerda do diagrama, nada está destacado e o valor do estado pai isFancy é falso. Na seção direita do diagrama, o valor do estado pai isFancy mudou para true e está destacado em amarelo, assim como a bolha de props abaixo, que também mudou seu valor isFancy para true.

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.

Pitfall

Lembre-se de que é a posição na árvore de UI — não no markup JSX — que importa para o React! Este componente tem duas cláusulas return com diferentes tags JSX <Counter /> dentro e fora do if:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <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>
  );
}

Você pode esperar que o estado seja redefinido ao marcar a caixa de seleção, mas não é! Isso ocorre porque ambas essas tags <Counter /> são renderizadas na mesma posição. O React não sabe onde você coloca as condições em sua função. Tudo o que ele “vê” é a árvore que você retorna.

Em ambos os casos, o componente App retorna um <div> com <Counter /> como o primeiro filho. Para o React, esses dois contadores têm o mesmo “endereço”: o primeiro filho do primeiro filho da raiz. É assim que o React os associa entre a renderização anterior e a próxima, independentemente de como você estrutura sua lógica.

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.

Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 3. A seção do meio tem o mesmo pai 'div', mas o componente filho foi agora deletado, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'p', destacado em amarelo.
Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 3. A seção do meio tem o mesmo pai 'div', mas o componente filho foi agora deletado, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'p', destacado em amarelo.

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

Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'p'. A seção do meio tem o mesmo pai 'div', mas o componente filho foi agora deletado, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, destacado em amarelo.
Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'p'. A seção do meio tem o mesmo pai 'div', mas o componente filho foi agora deletado, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, destacado em amarelo.

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.

Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'section', que tem um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 3. A seção do meio tem o mesmo pai 'div', mas os componentes filhos foram agora deletados, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'div', destacado em amarelo, também com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, tudo destacado em amarelo.
Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'section', que tem um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 3. A seção do meio tem o mesmo pai 'div', mas os componentes filhos foram agora deletados, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'div', destacado em amarelo, também com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, tudo destacado em amarelo.

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

Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'div', que tem um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0. A seção do meio tem o mesmo pai 'div', mas os componentes filhos foram agora deletados, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'section', destacado em amarelo, também com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, tudo destacado em amarelo.
Diagrama com três seções, com uma seta transicionando cada seção entre elas. A primeira seção contém um componente React rotulado 'div' com um único filho rotulado 'div', que tem um único filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0. A seção do meio tem o mesmo pai 'div', mas os componentes filhos foram agora deletados, indicado por uma imagem amarela de 'prova'. A terceira seção tem novamente o mesmo pai 'div', agora com um novo filho rotulado 'section', destacado em amarelo, também com um novo filho rotulado 'Counter' contendo uma bolha de estado rotulada 'count' com valor 0, tudo destacado em amarelo.

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.

Pitfall

É por isso que você não deve aninhar definições de funções de componentes.

Aqui, a função do componente MyTextField é definida dentro de MyComponent:

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

Toda vez que você clica no botão, o estado do input desaparece! Isso ocorre porque uma função MyTextField diferente é criada para cada renderização de MyComponent. Você está renderizando um componente diferente na mesma posição, então o React redefine todo o estado abaixo. Isso leva a bugs e problemas de desempenho. Para evitar esse problema, sempre declare funções de componentes no nível superior e não aninhe suas definições.

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:

  1. Renderizar componentes em posições diferentes
  2. 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 do Counter, 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.
Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'true'. O único filho, organizado à esquerda, é rotulado Counter com uma bolha de estado rotulada 'count' e valor 0. Todo o filho esquerdo está destacado em amarelo, indicando que foi adicionado.
Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'true'. O único filho, organizado à esquerda, é rotulado Counter com uma bolha de estado rotulada 'count' e valor 0. Todo o filho esquerdo está destacado em amarelo, indicando que foi adicionado.

Estado inicial

Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'false'. A bolha de estado está destacada em amarelo, indicando que foi alterada. O filho esquerdo é substituído por uma imagem de 'poof' amarela indicando que foi excluído e há um novo filho à direita, destacado em amarelo indicando que foi adicionado. O novo filho é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com o valor 0.
Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'false'. A bolha de estado está destacada em amarelo, indicando que foi alterada. O filho esquerdo é substituído por uma imagem de 'poof' amarela indicando que foi excluído e há um novo filho à direita, destacado em amarelo indicando que foi adicionado. O novo filho é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com o valor 0.

Clicando em “próximo”

Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'true'. A bolha de estado está destacada em amarelo, indicando que foi alterada. Há um novo filho à esquerda, destacado em amarelo indicando que foi adicionado. O novo filho é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com o valor 0. O filho direito é substituído por uma imagem de 'poof' amarela indicando que foi excluído.
Diagrama com uma árvore de componentes React. O pai é rotulado 'Scoreboard' com uma bolha de estado rotulada isPlayerA com o valor 'true'. A bolha de estado está destacada em amarelo, indicando que foi alterada. Há um novo filho à esquerda, destacado em amarelo indicando que foi adicionado. O novo filho é rotulado 'Counter' e contém uma bolha de estado rotulada 'count' com o valor 0. O filho direito é substituído por uma imagem de 'poof' amarela indicando que foi excluído.

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.

Note

Lembre-se de que as chaves não são globalmente exclusivas. Elas apenas especificam a posição dentro do pai.

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

Preservando o estado para componentes removidos

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 do localStorage e 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)}
    />
  );
}