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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_tree.dark.png&w=828&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_tree.png&w=828&q=75)
Á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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_increment.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_increment.png&w=1080&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_remove_component.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_remove_component.png&w=1080&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_add_component.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_add_component.png&w=1080&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_same_component.dark.png&w=1200&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_same_component.png&w=1200&q=75)
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.
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_pt1.dark.png&w=1920&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_pt1.png&w=1920&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_pt2.dark.png&w=1920&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_pt2.png&w=1920&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_same_pt1.dark.png&w=1920&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_same_pt1.png&w=1920&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_same_pt2.dark.png&w=1920&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_same_pt2.png&w=1920&q=75)
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.
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:
- Renderizar los componentes en diferentes posiciones
- 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
estrue
. Así que la primera posición contiene el estadoCounter
, 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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p1.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p1.png&w=1080&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p2.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p2.png&w=1080&q=75)
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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p3.dark.png&w=1080&q=75)
![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.](/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fpreserving_state_diff_position_p3.png&w=1080&q=75)
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.
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
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 delocalStorage
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)} /> ); }