Entendiendo la prop key de React
Traducido del original Understanding React's key prop creado por Kent C. Dodds.
Los ejemplos interactivos y el texto en inglés viven en el artículo original; aquí encontrarás la traducción al español. Si el tema te interesa, visita y comparte la publicación original.
Si prefieres verlo en vídeo, la lección complementaria es Use the key prop when Rendering a List with React en egghead.io, parte de The Beginner's Guide to ReactJS.
Un formulario que se comporta mal
En el artículo original hay un formulario con el que puedes jugar. Prueba a cambiar el asunto y después cambia el tema: verás que el valor del campo de texto no cambia por otro más razonable. Aunque escribas algo como «My company needs training» y luego cambies el tema de «Training» a «Question», tendría más sentido que el asunto se reiniciara a un valor por defecto mejor.
La segunda versión del formulario sí funciona como esperas. Esta es la implementación, con la diferencia resaltada:
const defaultValuesByTopic = {
training: "I would like some training",
consulting: "I have consulting needs",
question: "I have some questions",
};
function Contact() {
const [topic, setTopic] = React.useState("training");
return (
<form>
<label htmlFor="topic">Topic</label>
<select
id="topic"
value={topic}
onChange={(e) => setTopic(e.target.value)}
>
<option value="training">Training</option>
<option value="consulting">Consulting</option>
<option value="question">Question</option>
</select>
<label htmlFor="subject">Email Subject</label>
<input
id="subject"
key={topic}
defaultValue={defaultValuesByTopic[topic]}
/>
<label htmlFor="body">Email body</label>
<textarea id="body" />
</form>
);
}
La única diferencia entre ambas implementaciones es que la que funciona tiene una
prop key y la otra no.
Quiero compartir contigo un pequeño truco, no porque lo use mucho (aunque es
exactamente lo que hago en mi página de contacto),
sino porque entender este principio te ayudará a entender React un poco mejor.
Tiene que ver con las «instancias» de los componentes de React y con cómo React
trata la prop key.
Lo que voy a mostrarte tiene mucho que ver con las instancias de elementos y
componentes, y se aplica igual a los <input /> de arriba que a los componentes
que tú escribes y renderizas. Puede que sea algo más fácil de entender con el
estado de un componente, así que lo abordaremos desde ese ángulo.
Imagina que tienes un componente de React que gestiona estado interno. Ese estado va asociado a la instancia del componente. Por eso puedes renderizar ese componente dos veces en la página y ambos funcionarán de forma completamente independiente. Para la demostración, usemos algo realmente simple:
function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount((c) => c + 1);
return <button onClick={increment}>{count}</button>;
}
Podríamos renderizar esto muchas veces en la página y cada instancia sería completamente independiente. React guarda el estado junto a cada instancia concreta. Cuando se elimina un componente de la página, no afecta a los demás. Si renderizas uno nuevo, tampoco afecta a los existentes.
Puede que sepas que la prop key de React es algo que necesitas poner en los
elementos cuando recorres un array (si no, React se enfadará contigo).
Nota al margen: si quieres saber por qué esto es necesario y qué puede pasar si lo ignoras o simplemente pones el
indexcomo key, mira «Use the key prop when Rendering a List with React».
La prop key de React te da la capacidad de controlar las instancias de los
componentes. Cada vez que React renderiza tus componentes, está llamando a tus
funciones para obtener los nuevos elementos de React que usará para actualizar el
DOM. Si devuelves los mismos tipos de elemento, mantiene esos componentes y nodos
del DOM, incluso si todas las props cambiaron.
Para profundizar en esto, lee One simple trick to optimize React re-renders.
Ese asterisco sobre la palabra «todas» es de lo que quiero hablar aquí. La
excepción es la prop key. Te permite devolver exactamente el mismo tipo de
elemento, pero forzar a React a desmontar la instancia anterior y montar una
nueva. Esto significa que todo el estado que existía en el componente en ese
momento se elimina por completo y el componente queda «reinicializado» a todos
los efectos. Para los componentes, esto implica que React ejecutará la limpieza
de los efectos (o componentWillUnmount), y después los inicializadores de
estado (o el constructor) y las funciones de efecto (o componentDidMount).
NOTA: la limpieza del efecto ocurre en realidad después de que el nuevo componente se haya montado, pero antes de que se ejecute la siguiente función de efecto.
Aquí tienes un ejemplo sencillo funcionando en un contador:
function Counter() {
console.log("Counter called");
const [count, setCount] = React.useState(() => {
console.log("Counter useState initializer");
return 0;
});
const increment = () => setCount((c) => c + 1);
React.useEffect(() => {
console.log("Counter useEffect callback");
return () => {
console.log("Counter useEffect cleanup");
};
}, []);
console.log("Counter returning react elements");
return <button onClick={increment}>{count}</button>;
}
function CounterParent() {
// using useReducer this way basically ensures that any time you call
// setCounterKey, the `counterKey` is set to a new object which will
// make the `key` different resulting in React unmounting the previous
// component and mounting a new one.
const [counterKey, setCounterKey] = React.useReducer((c) => c + 1, 0);
return (
<div>
<button onClick={setCounterKey}>reset</button>
<Counter key={counterKey} />
</div>
);
}
Puedes ver ese contador funcionando en el artículo original.
Este es un ejemplo anotado de lo que se registraría en consola si pulso el botón del contador y después pulso «reset»:
// getting mounted
Counter called
Counter useState initializer
Counter returning react elements
// now it's mounted
Counter useEffect callback
// click the counter button
Counter called
Counter returning react elements
// notice the initializer and effect callback are not called this time
// click the reset button in the parent
// these next logs are happening for our new instance
Counter called
Counter useState initializer
Counter returning react elements
// cleanup old instance
Counter useEffect cleanup
// new instance is now mounted
Counter useEffect callback
Conclusión
De nuevo, esto ocurre igual con el estado de los elementos de formulario nativos
(para cosas como value e incluso el foco). La prop key no está solo para
librarte de ese molesto error de consola de React cuando intentas renderizar un
array de elementos (todos los errores «molestos» de React son estupendos y te
ayudan a evitar bugs, así que por favor no los ignores). La prop key también
puede ser un mecanismo útil para controlar las instancias de componentes y
elementos de React.
Espero que haya resultado interesante y esclarecedor. Si quieres jugar con este código, Kent tiene un codesandbox aquí. ¡Que te diviertas!