Conservar 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 conservar el estado y cuándo restablecerlo entre rerenderizados.
Aprenderás
- Cómo React “ve” las estructuras de los componentes
- Cuándo React elige preservar o reiniciar el estado
- Cómo forzar a React a restablecer el estado del componente
- Cómo las claves y los tipos afectan a la preservación del estado
El árbol de la UI
Los navegadores utilizan muchas estructuras de árbol para modelar la interfaz de usuario. El DOM representa los elementos HTML, el CSSOM hace lo mismo con el CSS. ¡Hay incluso un árbol de accesibilidad!
React también utiliza estructuras de árbol para gestionar y modelar la UI que estás generando. React crea árboles de UI a partir de su JSX. Posteriormente, React DOM actualiza los elementos del DOM del navegador para que coincidan con ese árbol UI. (React Native traduce estos árboles en elementos específicos para plataformas móviles).
State is tied to a position in the tree
Cuando se le da un estado a un componente, podrías pensar que el estado “vive” dentro del componente. Pero en realidad el estado se mantiene en React. React asocia cada pieza de estado que mantiene con el componente correcto gracias al lugar que ocupa ese componente en el árbol de la UI.
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)}> Add one </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)}> Add one </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. Para ver esto, incrementa ambos contadores, luego quita el segundo componente desmarcando la casilla “Render the second counter”, 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) }} /> 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> ); }
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 conserva 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) }} /> 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> ); }
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>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> ); }
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) }} /> 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> ); }
El estado del contador se restablece 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); }}> Next player! </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>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </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 restablecer 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); }}> Next player! </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>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </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 “Next player”, 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: Restablecer el estado con key
También hay otra forma, más genérica, de restablecer 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); }}> Next player! </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>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
El cambio entre Taylor y Sarah no conserva el estado. Esto se debe a que le asignaste diferentes key
s:
{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.
Recuerda que las keys no son únicas globalmente. Sólo especifican la posición dentro del padre.
Restablecer un formulario con una key
Restablecer 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' } ];
Deep Dive
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 restablecer su estado dándole una key diferente.
- No anides las definiciones de los componentes, o restablecerá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 restablezca el texto de 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)} /> ); }