Preservar y reiniciar el estado

El estado está aislado entre los componentes. React mantiene un registro de qué estado pertenece a qué componente basándose en su lugar en el árbol de la interfaz de usuario (UI). Puedes controlar cuándo preservar el estado y cuándo reiniciarlo entre rerenderizados.

Aprenderás

  • When React chooses to preserve or reset the state
  • How to force React to reset component’s state
  • How keys and types affect whether the state is preserved

El estado está atado a la posición en el árbol de renderizado

React construye árboles de renderizado para la estructura de componentes en tu UI.

Cuando le das estado a tu componente, podrías pensar que el estado «vive» dentro del componente. Pero el estado en realidad se guarda dentro de React. React asocia cada pieza de estado que mantiene con el componente correcto por la posición en la que se encuentra ese componente en el árbol de renderizado.

En este caso, sólo hay una etiqueta JSX <Counter />, pero se representa en dos posiciones 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)}>
        Agregar uno
      </button>
    </div>
  );
}

Esta sería la apariencia del árbol:

Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. Cada uno de los hijos está etiquetado como 'Counter' y ambos contienen una burbuja de estado etiquetada como 'count' con valor 0.
Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. Cada uno de los hijos está etiquetado como 'Counter' y ambos contienen una burbuja de estado etiquetada como 'count' con valor 0.

Árbol de React

Son dos contadores separados porque cada uno se renderiza en su propia posición en el árbol. Normalmente no tienes que pensar en estas posiciones para usar React, pero puede ser útil para entender cómo funciona.

En React, cada componente en la pantalla tiene un estado totalmente aislado. Por ejemplo, si renderizas dos componentes Counter, uno al lado del otro, cada uno de ellos obtendrá sus propios e independientes estados score y hover.

Prueba a hacer clic en ambos contadores y observa que no se afectan mutuamente:

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)}>
        Agregar uno
      </button>
    </div>
  );
}

Como puedes ver, cuando se actualiza un contador, sólo se actualiza el estado de ese componente:

Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo derecho se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 1. La burbuja de estado del hijo derecho está resaltada en amarillo para indicar que su valor se ha actualizado.
Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo derecho se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 1. La burbuja de estado del hijo derecho está resaltada en amarillo para indicar que su valor se ha actualizado.

Actualización del estado

React mantendrá el estado mientras se renderice el mismo componente en la misma posición en el árbol. Para ver esto, incrementa ambos contadores, luego quita el segundo componente desmarcando la casilla «Renderizar el segundo contador», y luego vuelve a añadirlo marcándola de nuevo:

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)
          }}
        />
        Renderizar el segundo contador
      </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)}>
        Agregar uno
      </button>
    </div>
  );
}

Observa cómo en el momento en que dejas de renderizar el segundo contador, su estado desaparece por completo. Eso es porque cuando React elimina un componente, destruye su estado.

Diagrama de un árbol de componentes React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo de la derecha no está, y en su lugar hay una imagen amarilla '¡puf!', destacando el componente que se está eliminando del árbol.
Diagrama de un árbol de componentes React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo de la derecha no está, y en su lugar hay una imagen amarilla '¡puf!', destacando el componente que se está eliminando del árbol.

Eliminación de un componente

Al marcar «Renderizar el segundo contador», se inicializa un segundo Counter y su estado se inicializa desde cero (score = 0) y se añade al DOM.

Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo derecho se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. Todo el nodo hijo derecho está resaltado en amarillo, indicando que acaba de ser añadido al árbol.
Diagrama de un árbol de componentes de React. El nodo raíz está etiquetado como 'div' y tiene dos hijos. El hijo izquierdo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo derecho se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. Todo el nodo hijo derecho está resaltado en amarillo, indicando que acaba de ser añadido al árbol.

Añadiendo un componente

React preserva el estado de un componente mientras se renderiza en su posición en el árbol de la interfaz de usuario. Si se elimina, o se renderiza un componente diferente en la misma posición, React descarta su estado.

El mismo componente en la misma posición preserva el estado

En este ejemplo, hay dos tipos diferentes de etiquetas <Counter />:

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)
          }}
        />
        Usar un estilo elegante
      </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)}>
        Agregar uno
      </button>
    </div>
  );
}

Cuando se marca o desactiva la casilla, el estado del contador no se reinicia. Tanto si isFancy es true como si es false, siempre tendrás un <Counter /> como primer hijo del div devuelto desde el componente raíz App:

Diagrama con dos secciones separadas por una flecha de transición entre ellas. Cada sección contiene un diseño de componentes con un padre etiquetado como 'App' que contiene una burbuja de estado etiquetada como isFancy. Este componente tiene un hijo etiquetado 'div', que lleva a una burbuja de prop que contiene isFancy (resaltada en púrpura) que pasa al único hijo. El último hijo se llama 'Counter'y contiene una burbuja de estado con la etiqueta 'count' y el valor 3 en ambos diagramas. En la sección izquierda del diagrama, no hay nada resaltado y el valor de estado del padre isFancy es falso. En la sección derecha del diagrama, el valor del estado padre isFancy ha cambiado a verdadero y está resaltado en amarillo, al igual que la burbuja de props que está debajo, que también ha cambiado su valor isFancy a verdadero.
Diagrama con dos secciones separadas por una flecha de transición entre ellas. Cada sección contiene un diseño de componentes con un padre etiquetado como 'App' que contiene una burbuja de estado etiquetada como isFancy. Este componente tiene un hijo etiquetado 'div', que lleva a una burbuja de prop que contiene isFancy (resaltada en púrpura) que pasa al único hijo. El último hijo se llama 'Counter'y contiene una burbuja de estado con la etiqueta 'count' y el valor 3 en ambos diagramas. En la sección izquierda del diagrama, no hay nada resaltado y el valor de estado del padre isFancy es falso. En la sección derecha del diagrama, el valor del estado padre isFancy ha cambiado a verdadero y está resaltado en amarillo, al igual que la burbuja de props que está debajo, que también ha cambiado su valor isFancy a verdadero.

La actualización del estado de la App no reinicia el Counter porque el Counter permanece en la misma posición

Es el mismo componente en la misma posición, por lo tanto desde la perspectiva de React, es el mismo contador.

Atención

¡Recuerda que es la posición en el árbol de la UI —no en el markup JSX— lo que le importa a React! Este componente tiene dos cláusulas return con diferentes etiquetas JSX <Counter /> dentro y fuera del 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)
            }}
          />
          Usar un estilo elegante
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Usar un estilo elegante
      </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)}>
        Agregar uno
      </button>
    </div>
  );
}

Se podría esperar que el estado se reiniciara al marcar la casilla de verificación, pero no es así. Esto se debe a que las dos etiquetas <Counter /> se renderizan en la misma posición. React no sabe dónde colocas las condiciones en tu función. Todo lo que «ve» es el árbol que devuelves. En ambos casos, el componente App devuelve un <div> con <Counter /> como primer hijo. Por eso React los considera como el mismo <Counter />.

Puedes pensar que tienen la misma «dirección»: el primer hijo del primer hijo de la raíz. Así es como React los hace coincidir entre los renderizados anteriores y los siguientes, independientemente de cómo estructures tu lógica.

Diferentes componentes en la misma posición reinician el estado

En este ejemplo, al marcar la casilla de verificación se sustituirá <Counter> por un <p>:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>¡Nos vemos luego!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Tómate un descanso
      </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)}>
        Agregar uno
      </button>
    </div>
  );
}

Aquí se cambia entre diferentes tipos de componentes en la misma posición. Inicialmente, el primer hijo del <div> contenía un Counter. Pero cuando lo cambiaste por un p, React eliminó el Counter del árbol de la UI y destruyó su estado.

Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente React etiquetado 'div' con un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 3. La sección del medio tiene el mismo padre 'div', pero el componente hijo ha sido eliminado, indicado por una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', pero con un nuevo hijo llamado 'p', resaltado en amarillo.
Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente React etiquetado 'div' con un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 3. La sección del medio tiene el mismo padre 'div', pero el componente hijo ha sido eliminado, indicado por una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', pero con un nuevo hijo llamado 'p', resaltado en amarillo.

Cuando Counter cambia a p, se borra el Counter y se añade p

Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado como 'p'. La sección del medio tiene el mismo padre 'div', pero el componente hijo ha sido eliminado, indicado por una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div' de nuevo, ahora con un nuevo hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 0, resaltada en amarillo.
Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado como 'p'. La sección del medio tiene el mismo padre 'div', pero el componente hijo ha sido eliminado, indicado por una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div' de nuevo, ahora con un nuevo hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 0, resaltada en amarillo.

Al volver a cambiar, se borra p y se añade el Counter.

Además, cuando se renderiza un componente diferente en la misma posición, se reinicia el estado de todo su subárbol. Para ver cómo funciona, incrementa el contador y luego marca la casilla:

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)
          }}
        />
        Usar un estilo elegante
      </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)}>
        Agregar uno
      </button>
    </div>
  );
}

El estado del contador se reinicia cuando se hace clic en la casilla de verificación. Aunque se renderiza un Counter, el primer hijo del div cambia de div a section. Cuando el div hijo se eliminó del DOM, todo el árbol debajo de él (incluyendo el Counter y su estado) se destruyó también.

Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado 'div' con un único hijo etiquetado 'section', que tiene un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 3. La sección del medio tiene el mismo padre 'div', pero los componentes hijos se han eliminado, lo que se indica con una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', ahora con un nuevo hijo llamado 'div', resaltado en amarillo, también con un nuevo hijo llamado 'Counter' que contiene una burbuja de estado llamada 'count' con valor 0, todo resaltado en amarillo.
Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado 'div' con un único hijo etiquetado 'section', que tiene un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 3. La sección del medio tiene el mismo padre 'div', pero los componentes hijos se han eliminado, lo que se indica con una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', ahora con un nuevo hijo llamado 'div', resaltado en amarillo, también con un nuevo hijo llamado 'Counter' que contiene una burbuja de estado llamada 'count' con valor 0, todo resaltado en amarillo.

Cuando section cambia a div, se elimina la section y se añade el nuevo div

Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado 'div' con un único hijo etiquetado 'div', que tiene un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 0. La sección del medio tiene el mismo padre 'div', pero los componentes hijos se han eliminado, lo que se indica con una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', ahora con un nuevo hijo llamado 'section', resaltado en amarillo, también con un nuevo hijo llamado 'Counter' que contiene una burbuja de estado llamada 'count' con valor 0, todo resaltado en amarillo.
Diagrama con tres secciones, con una flecha de transición entre cada sección. La primera sección contiene un componente de React etiquetado 'div' con un único hijo etiquetado 'div', que tiene un único hijo etiquetado 'Counter' que contiene una burbuja de estado etiquetada 'count' con valor 0. La sección del medio tiene el mismo padre 'div', pero los componentes hijos se han eliminado, lo que se indica con una imagen amarilla '¡puf!'. La tercera sección tiene el mismo padre 'div', ahora con un nuevo hijo llamado 'section', resaltado en amarillo, también con un nuevo hijo llamado 'Counter' que contiene una burbuja de estado llamada 'count' con valor 0, todo resaltado en amarillo.

Al volver a cambiar, se elimina el div y se añade la nueva section.

Como regla general, si quieres preservar el estado entre rerenderizados, la estructura de tu árbol necesita «coincidir» de un render a otro. Si la estructura es diferente, el estado se destruye porque React destruye el estado cuando elimina un componente del árbol.

Atención

Es por este motivo que no se deben anidar las definiciones de las funciones de los componentes.

Aquí, la función del componente MyTextField se define 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)
      }}>Hiciste clic {counter} veces</button>
    </>
  );
}

Cada vez que se hace clic en el botón, el estado de la entrada desaparece. Esto se debe a que se crea una función diferente de MyTextField para cada renderizado de MyComponent. Estás renderizando un componente diferente en la misma posición, por lo que React reinicia todo el estado que esté anidado por debajo. Esto conlleva a errores y problemas de rendimiento. Para evitar este problema, declara siempre las funciones del componente en el nivel superior, y no anides sus definiciones.

Reiniciar el estado en la misma posición

Por defecto, React preserva el estado de un componente mientras permanece en la misma posición. Normalmente, esto es exactamente lo que quieres, así que tiene sentido como comportamiento por defecto. Pero a veces, es posible que quieras reiniciar el estado de un componente. Considera esta aplicación que permite a dos jugadores llevar la cuenta de sus puntuaciones durante 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);
      }}>
        ¡Siguiente jugador!
      </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>Puntos de {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Agregar uno
      </button>
    </div>
  );
}

Actualmente, cuando se cambia de jugador, la puntuación se conserva. Los dos Counter aparecen en la misma posición, por lo que React los ve como el mismo Counter cuya prop person ha cambiado.

Pero conceptualmente, en esta aplicación deberían ser dos contadores separados. Podrían aparecer en el mismo lugar en la UI, pero uno es un contador para Taylor, y otro es un contador para Sarah.

Hay dos maneras de reiniciar el estado al cambiar entre ellos:

  1. Renderizar los componentes en diferentes posiciones
  2. Dar a cada componente una identidad explícita con key.

Opción 1: Renderizar un componente en diferentes posiciones

Si quieres que estos dos Counter sean independientes, puedes representarlos en dos posiciones 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);
      }}>
        ¡Siguiente jugador!
      </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>Puntos de {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Agregar uno
      </button>
    </div>
  );
}

  • Inicialmente, isPlayerA es true. Así que la primera posición contiene el estado Counter, y la segunda está vacía.
  • Cuando haces clic en el botón «Siguiente jugador», la primera posición se borra, pero la segunda contiene ahora un ‘Counter’.
Diagrama con un árbol de componentes React. El padre está etiquetado como ’Scoreboard' con una burbuja de estado etiquetada como isPlayerA con valor 'true'. El único hijo, dispuesto a la izquierda, se llama Counter con una burbuja de estado llamada 'count' y valor 0. Todo el hijo de la izquierda está resaltado en amarillo, indicando que fue añadido.
Diagrama con un árbol de componentes React. El padre está etiquetado como ’Scoreboard' con una burbuja de estado etiquetada como isPlayerA con valor 'true'. El único hijo, dispuesto a la izquierda, se llama Counter con una burbuja de estado llamada 'count' y valor 0. Todo el hijo de la izquierda está resaltado en amarillo, indicando que fue añadido.

Estado inicial

Diagrama con un árbol de componentes de React. El padre está etiquetado como 'Marcador' con una burbuja de estado etiquetada como isPlayerA con valor 'false'. La burbuja de estado está resaltada en amarillo, indicando que ha cambiado. El hijo de la izquierda es reemplazado por una imagen amarilla '¡puf!' que indica que ha sido eliminado y hay un nuevo hijo a la derecha, resaltado en amarillo indicando que fue agregado. El nuevo hijo se denomina 'Counter' y contiene una burbuja de estado denominada 'count' con valor 0.
Diagrama con un árbol de componentes de React. El padre está etiquetado como 'Marcador' con una burbuja de estado etiquetada como isPlayerA con valor 'false'. La burbuja de estado está resaltada en amarillo, indicando que ha cambiado. El hijo de la izquierda es reemplazado por una imagen amarilla '¡puf!' que indica que ha sido eliminado y hay un nuevo hijo a la derecha, resaltado en amarillo indicando que fue agregado. El nuevo hijo se denomina 'Counter' y contiene una burbuja de estado denominada 'count' con valor 0.

Pulsando «next»

Diagrama con un árbol de componentes React. El padre está etiquetado como 'Scoreboard' con una burbuja de estado etiquetada como isPlayerA con valor 'true'. La burbuja de estado está resaltada en amarillo, indicando que ha cambiado. Hay un nuevo hijo a la izquierda, resaltado en amarillo indicando que se ha añadido. El nuevo hijo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo de la derecha es reemplazado por una imagen amarilla '¡puf!' que indica que ha sido eliminado.
Diagrama con un árbol de componentes React. El padre está etiquetado como 'Scoreboard' con una burbuja de estado etiquetada como isPlayerA con valor 'true'. La burbuja de estado está resaltada en amarillo, indicando que ha cambiado. Hay un nuevo hijo a la izquierda, resaltado en amarillo indicando que se ha añadido. El nuevo hijo se llama 'Counter' y contiene una burbuja de estado llamada 'count' con valor 0. El hijo de la derecha es reemplazado por una imagen amarilla '¡puf!' que indica que ha sido eliminado.

Pulsando «next» de nuevo

El estado de cada Counter se destruye cada vez que se elimina del DOM. Por eso se reinician cada vez que se hace clic en el botón.

Esta solución es conveniente cuando sólo tienes unos pocos componentes independientes renderizados en el mismo lugar. En este ejemplo, sólo tienes dos, por lo que no es una molestia renderizar ambos por separado en el JSX.

Option 2: Opción 2: Reiniciar el estado con una key

También hay otra forma, más genérica, de reiniciar el estado de un componente.

Es posible que hayas visto key al renderizar listas. Las keys no son sólo para las listas. Puedes usar keys para que React distinga entre cualquier componente. Por defecto, React utiliza el orden dentro del padre («primer contador», «segundo contador») para discernir entre los componentes. Pero las keys te permiten decirle a React que no es sólo un primer contador, o un segundo contador, sino un contador específico; por ejemplo, el contador de Taylor. De esta manera, React conocerá el contador de Taylor dondequiera que aparezca en el árbol!

En este ejemplo, los dos <Counter /> no comparten estado aunque aparezcan en el mismo lugar en 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);
      }}>
        ¡Siguiente jugador!
      </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>Puntos de {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Agregar uno
      </button>
    </div>
  );
}

El cambio entre Taylor y Sarah no preserva el estado. Esto se debe a que le asignaste diferentes keys:

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

Especificar una key le dice a React que use la propia key como parte de la posición, en lugar de su orden dentro del padre. Por eso, aunque los renderices en el mismo lugar en JSX, desde la perspectiva de React, son dos contadores diferentes. Como resultado, nunca compartirán estado. Cada vez que un contador aparece en la pantalla, su estado se crea. Cada vez que se elimina, su estado se destruye. Alternar entre ellos reinicia su estado una y otra vez.

Nota

Recuerda que las keys no son únicas globalmente. Sólo especifican la posición dentro del padre.

Reiniciar un formulario con una key

Reiniciar el estado con una key es especialmente útil cuando se trata de formularios.

En esta aplicación de chat, el componente <Chat> contiene el estado del cuadro 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' }
];

Prueba a introducir algo en el cuadro de texto y luego pulsa «Alice» o «Bob» para elegir un destinatario diferente. Notarás que el estado del cuadro de texto se conserva porque el <Chat> se renderiza en la misma posición en el árbol.

En muchas aplicaciones, este puede ser el comportamiento deseado, pero no en una aplicación de chat!. No quieres que el usuario envíe un mensaje que ya ha escrito a una persona equivocada debido a un clic accidental. Para solucionarlo, añade una key:

<Chat key={to.id} contact={to} />

Esto asegura que cuando selecciones un destinatario diferente, el componente Chat se recreará desde cero, incluyendo cualquier estado en el árbol que esté por debajo. React también recreará los elementos del DOM en lugar de reutilizarlos.

Ahora al cambiar de destinatario siempre se borra el 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' }
];

Profundizar

Preservar el estado de los componentes removidos

En una aplicación de chat real, probablemente querrás recuperar el estado de la entrada cuando el usuario vuelva a seleccionar el destinatario anterior. Hay algunas maneras de mantener el estado «vivo» para un componente que ya no es visible:

  • Podrías mostrar todos los chats en lugar de sólo el actual, pero ocultar todos los demás con CSS. Los chats no se eliminarían del árbol, por lo que su estado local se conservaría. Esta solución funciona muy bien para UIs simples. Pero puede ser muy lenta si los árboles ocultos son grandes y contienen muchos nodos DOM.
  • Podrías subir el estado y mantener el mensaje pendiente para cada destinatario en el componente padre. De esta manera, cuando los componentes hijos se eliminan, no importa, porque es el padre el que mantiene la información importante. Esta es la solución más común. También podrías utilizar una fuente diferente además del estado de React. Por ejemplo, probablemente quieras que el borrador de un mensaje persista incluso si el usuario cierra accidentalmente la página. Para implementar esto, podrías hacer que el componente Chat inicialice su estado leyendo de localStorage y guardar los borradores allí también.

Independientemente de la estrategia que elijas, un chat con Alice es conceptualmente distinto de un chat con Bob, por lo que tiene sentido dar una key al árbol <Chat> basado en el destinatario actual.

Recapitulación

  • React mantiene el estado mientras el mismo componente se renderice en la misma posición.
  • El estado no se mantiene en las etiquetas JSX. Se asocia a la posición del árbol en la que se coloca ese JSX.
  • Puedes forzar a un subárbol a reiniciar su estado dándole una key diferente.
  • No anides las definiciones de los componentes, o reiniciarás el estado por accidente.

Desafío 1 de 5:
Corregir la desaparición del texto de entrada

Este ejemplo muestra un mensaje cuando se pulsa el botón. Sin embargo, al pulsar el botón también se reinicia accidentalmente la entrada. ¿Por qué ocurre esto? Arréglalo para que al pulsar el botón no se reinicie el texto de entrada.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Pista: ¿Tu ciudad favorita?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Ocultar pista</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Mostrar pista</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}