Entendiendo la prop key de React

Código de React comparando un input sin key con el mismo input con key={topic}, que reinicia su defaultValue

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 index como 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!

  • El patrón inicializador de estado.

    Un patrón simple usado en librerías tales como downshift y ReachUI para habilitar usuarios de un componente/hook, la inicialización o reinicio de tu estado a ese valor inicial.