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:
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:
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.
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.
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
:
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.
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.
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’.
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)} /> ); }