Uno de los conceptos más importantes que todo desarrollador de React debe comprender es el estado: qué es, cómo usarlo correctamente y cómo evitar los errores comunes a medida que construye sus aplicaciones.

Vamos a cubrir cinco de las partes más esenciales del estado que necesitas saber. Cada una de estas partes se basa en la otra para ayudarte a comprender en general un tema algo complejo.

Para que estos conceptos abstractos sean lo más claros posible, he incluido muchos ejemplos prácticos que puede ejecutar en Code Sandbox o en cualquier proyecto de React que hayas configurado.

1. Las actualizaciones de estado con useState no se combinan

Un desafío al que se enfrentan muchos desarrolladores de React al pasar de componentes basados ​​en clases a componentes de funciones con hooks de React es que las actualizaciones de estado que utilizan objetos ya no se fusionan automáticamente.

Una gran ventaja del hook useState es que podemos llamarlo tantas veces como queramos para usar tantas variables de estado como necesitemos.

En este ejemplo, tenemos un formulario básico con una entrada de correo electrónico y contraseña. Gestionamos el estado del correo electrónico y la contraseña como variables de estado individuales:

import React from "react";

export default function App() {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  return (
    <form>
      <input
        name="email"
        type="email"
        placeholder="Email"
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        name="password"
        type="password"
        placeholder="Password"
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Cambiemos nuestro ejemplo para administrar nuestro estado de formulario dentro de un solo objeto. Esto nos permite llamar a useState solo una vez, donde el correo electrónico y la contraseña no son administrados por variables de estado individuales sino como propiedades de esta variable de estado llamada state.

¿Cómo actualizamos adecuadamente el estado con la función setState cuando es un objeto?

Si usáramos un controlador de eventos genérico que está conectado a la propiedad onChange de cada una de las entradas de nuestro formulario, se vería así:

import React from "react";

export default function App() {
  const [state, setState] = React.useState({
    email: '',
    password: ''
  })

  function handleInputChange(e) {
    setState({
      [e.target.name]: e.target.value
    })
  }

  return (
    <form>
      <input
        name="email"
        type="email"
        onChange={handleInputChange}
      />
      <input
        name="password"
        type="password"
        onChange={handleInputChange}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Ahora estamos actualizando el valor de cada entrada en estado de acuerdo con el nombre de la entrada que nuestro usuario está escribiendo actualmente.

Este patrón se usa comúnmente para actualizar el estado en componentes basados ​​en clases, pero esto no funciona con el hook useState. Las actualizaciones de estado con la función setState de useState no se combinan automáticamente.

¿Qué significa eso?

Significa que siempre que establecemos el estado a medida que nuestro usuario escribe, el estado anterior no se incluye en el nuevo estado. Si tuviéramos que registrar nuestro estado recién actualizado mientras escribimos en nuestro formulario, vemos lo siguiente:

5-things-every-react-developer-should-know-about-state-1

Dado que el estado anterior no se fusiona automáticamente con el nuevo objeto de estado, debemos fusionar manualmente nuestro objeto de estado con sus propiedades anteriores utilizando el operador de propagación del objeto:

import React from "react";

export default function App() {
  const [state, setState] = React.useState({
    email: '',
    password: ''
  })

  function handleInputChange(e) {
    setState({
      // spread in previous state with object spread operator
      ...state,
      [e.target.name]: e.target.value
    })
  }

  return (
    <form>
      <input
        name="email"
        type="email"
        onChange={handleInputChange}
      />
      <input
        name="password"
        type="password"
        onChange={handleInputChange}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Para el hook useState, tenemos la flexibilidad de administrar múltiples valores primitivos o usar un objeto con múltiples propiedades.

Sin embargo, si usas useState con un objeto, recuerda propagar en el estado anterior cuando se realice cualquier actualización para asegurarse de que se actualice correctamente.

2. Los hooks de estado desencadenan una re-renderización, useRef no

El estado de reacción tiene una relación muy importante con los componentes de renderizado.

Siempre que devolvemos JSX desde un componente de React, cuando se usa ese componente, se renderizará y, por lo tanto, se mostrará en nuestra aplicación. React se encarga de este proceso de renderizado.

Si nuestro componente usa cualquier estado, debemos entender que debe re-renderizarse, es decir, volver a renderizar, en respuesta a cualquier actualización de estado.

¿Por qué es necesario volver a renderizar los componentes en las actualizaciones de estado?

Porque si no volvemos a renderizar al actualizar el estado, no podríamos mostrar nuevos datos. Esto se expresa de manera muy simple, siempre que mostramos cualquier estado contenido dentro de una variable de estado dentro de nuestro JSX.

Si no se volviera a renderizar cada vez que realizamos cambios en esa variable, las actualizaciones no se mostrarían.

Esto parece un concepto bastante simple, pero debes comprender que cada vez que actualizamos el estado, no solo provoca una nueva renderización en el componente que administra directamente el estado, sino que también provoca una nueva renderización en todos los componentes secundarios.

¿Por qué es importante esto? Porque en algunos casos, es posible que no queramos que un componente secundario se vuelva a renderizar en respuesta a una nueva representación del componente principal.

¿Cuál es uno de esos ejemplos? Digamos que tenemos una aplicación en la que un usuario puede escribir en una entrada cuyo valor se administra a través del estado. Esta aplicación también tiene otro componente que muestra una lista de datos.

Siempre que el usuario escribe en la entrada, nuestro estado se actualiza y esto provoca una re-renderización innecesaria en ese otro componente hijo.

La forma en que podemos solucionar esto es con la ayuda de la función React.memo, que ayuda a evitar que nuestro componente se vuelva a renderizar cuando un componente padre se vuelve a renderizar:

export default function App() {
  const [skill, setSkill] = React.useState("");
  const [skills, setSkills] = React.useState(["HTML", "CSS", "JavaScript"]);

  function handleChangeInput(event) {
    setSkill(event.target.value);
  }

  function handleAddSkill() {
    setSkills(skills.concat(skill));
  }

  return (
    <>
      <input onChange={handleChangeInput} />
      <button onClick={handleAddSkill}>Add Skill</button>
      <SkillList skills={skills} />
    </>
  );
}

/* Pero el problema, si ejecutas este código tu mismo, es que cuando escribimos en la entrada, debido a que el componente principal de SkillList (App) vuelve a renderizarse, debido a que el estado se actualiza con cada pulsación de tecla, SkillList se vuelve a renderizar constantemente (como lo indicada console.log) */

/* Sin embargo, una vez que envolvemos el componente SkillList en React.memo (que es una función de orden superior, lo que significa que acepta una función como argumento), ya no se vuelve a renderizar innecesariamente cuando lo hace nuestro componente padre. */
const SkillList = React.memo(({ skills }) => {
  console.log("rerendering");
  return (
    <ul>
      {skills.map((skill, i) => (
        <li key={i}>{skill}</li>
      ))}
    </ul>
  );
});

Otra cosa a tener en cuenta aquí es que técnicamente hay una forma de administrar el estado sin causar una nueva renderización. Podemos hacerlo con un hook que la mayoría de la gente no ve como un gancho de React con estado: useRef.

useRef se puede utilizar para almacenar cualquier valor en su propiedad .current. En otras palabras, si quisiéramos hacer un contador simple con useRef y actualizar un valor de recuento que almacenamos en él, incluso si actualizamos su valor, no mostraría el recuento correcto después del render inicial porque hacerlo no activa un re-render:

import React from "react";

export default function App() {
  const countRef = React.useRef(0);

  function handleAddOne() {
    countRef.current += 1;
  }

  return (
    <>
      <h1>Count: {countRef.current}</h1>

      {/* clicking this will not change display count */}
      <button onClick={handleAddOne}>+ 1</button>
    </>
  );
}

3. Las actualizaciones de estado deben ser inmutables

Una parte muy importante del estado en React es que debe actualizarse y administrarse de la manera correcta.

Cuando se trata de administrar el estado con el hook useState, debemos usar solo la función de useState dedicado a modificar el estado y se proporciona como el segundo elemento en el arreglo que obtenemos de useState para actualizarlo. Si no lo hacemos e intentamos actualizarlo manualmente, por ejemplo, con la ayuda de JavaScript plano nuestra aplicación no funcionará como esperamos.

Este punto está muy relacionado con el punto anterior que hicimos: el estado, cuando se actualiza correctamente, provoca una re-renderización de nuestro componente.

¿Qué crees que pasará si intentamos actualizar el estado a nuestra manera en lugar de "React"?

Una vez más, React es el que se encarga de mostrar y renderizar nuestro componente correctamente cuando algo cambia. Si no usamos React, entonces no podemos esperar que nuestra aplicación refleje los cambios que hicimos en el estado.

En otras palabras, si actualizamos el estado con JavaScript plano y no setState, no activará una nueva renderización y React no mostrará esos cambios (inválidos) en el estado a nuestro usuario.

Esta es una lección simple pero crucial para recordar.

Debemos saber cómo actualizar el estado usando React y elegir el hook de estado apropiado para nuestros propósitos. Podríamos elegir useReducer, useState o una biblioteca de administración de estado de terceros como Redux.

Independientemente de nuestra elección en la gestión del estado, debemos actualizar el estado de la manera adecuada y no intentar actualizarlo o modificarlo directamente.

La otra razón de esto, además de que nuestra aplicación React no funciona correctamente, es que viola un principio básico de React. Este es el concepto de inmutabilidad.

Las actualizaciones de estado siempre deben ser inmutables. Esto significa que no deberíamos hacer nuestros propios cambios o mutar los datos almacenados en nuestras variables de estado. Hacerlo hace que nuestro estado sea impredecible y puede causar problemas no deseados en nuestra aplicación que son difíciles de depurar.

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0);
  
  // No asigne estado a nuevas variables (que no usen el hook useState)
  const newCount = count;
  // No mutes directamente el estado
  const countPlusOne = count + 1;

  return (
    <>
      <h1>Count: {count}</h1>
    </>
  );
}

Además de no mutar las variables de estado directamente, asegúrese de no asignar nunca variables de estado a otras variables (que no usen el hook useState).

4. Las actualizaciones de estado son asincrónicas y programadas

Una lección crucial que debe saber sobre las actualizaciones del estado es que no se realizan de inmediato.

Esto se puede ver si echamos un vistazo a la documentación de React y vemos exactamente qué sucede cuando llamamos a la función setState. Lo usamos para actualizar la variable de estado asociada con él, pero también se nos dice:

Acepta un nuevo valor de estado y pone en cola (enqueues) una reproducción del componente.

¿Qué significa esta palabra "enqueues"?

En otras palabras, no vuelve a renderizar el componente inmediatamente. No detiene nuestro código justo en esa línea donde actualizamos el estado, pero tiene lugar en algún momento en el futuro. Esto es por motivos de rendimiento y esto nos da una mejor idea de lo que React está haciendo bajo el capó.

Basándonos en esta información, necesitamos cambiar nuestro modelo mental cuando intentamos actualizar el estado: la función setState no actualiza inmediatamente el estado, simplemente programa una actualización de estado para algún tiempo en el futuro. Después de lo cual, React se encarga de averiguar cuándo se lleva a cabo esa actualización de estado.

Por lo tanto, no es tan fácil poder mirar nuestro código y ver exactamente cuándo ocurrió o ocurrirá la actualización de estado.

Es importante comparar esto con useRef, que mencionamos anteriormente como capaz de retener los datos dentro de su propiedad actual. Cualquier actualización realizada con useRef se realiza de forma sincrónica; podemos mirar nuestro código y ver exactamente cuándo se realizó una actualización determinada en useRef, pero no con useState.

5. El estado obsoleto puede ocurrir con los closures

Finalmente, un problema importante que puede ocurrir con el estado React es el problema del estado obsoleto.

¿Qué es el estado obsoleto en React?

El estado obsoleto es un problema que ocurre cada vez que intentamos actualizar el estado, a menudo dentro de un closure.

Un closure es un tipo de función en JavaScript, donde usamos una variable de un ámbito externo.

Este problema del estado obsoleto se basa en el hecho de que el cierre podría no capturar el valor de la variable de estado más actualizado. Eso es lo que queremos decir con obsoleto: queremos decir que es antiguo y no el valor actual que queremos.

Este problema de estado obsoleto está estrechamente relacionado con el tema que discutimos anteriormente de las actualizaciones de estado son asincrónicas.

En muchos casos, el problema de que las actualizaciones de estado sean asincrónicas es que no siempre obtenemos el valor anterior correcto de nuestro estado, especialmente si intentamos actualizar el estado en función de ese valor anterior.

Podemos expresar el problema de un closure obsoleto dentro de una aplicación de contador simple que actualiza el recuento después de un segundo usando la función setTimeout.

Debido a que setTimeout crea un closure, estamos accediendo a un valor obsoleto de nuestra variable de estado, count, cuando llamamos a setCount.

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0);

  function delayAddOne() {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  }

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={delayAddOne}>+ 1</button>
    </>
  );
}

El problema es evidente cuando ejecutamos nuestra aplicación. A pesar de hacer clic en el botón varias veces, solo se incrementa en uno por segundo:

5-thing-every-react-developer-should-know-about-state-2

Podemos solucionar este problema de nuestro estado obsoleto dentro de nuestro closure utilizando un método más confiable para actualizar el estado. Las actualizaciones de estado todavía se van a programar, pero permitirán obtener de forma fiable el valor de estado anterior.

Hacemos esto con la ayuda de proporcionar una función interna a la función setState. En el cuerpo de la función, podemos obtener el estado anterior dentro de los parámetros de esta función y luego devolver lo que queremos que sea el siguiente estado.

En nuestro caso, será el valor de recuento anterior incrementado en uno:

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0);

  function delayAddOne() {
    setTimeout(() => {
      // El problema del estado obsoleto desaparece usando una función interna
      setCount(prevCount => prevCount + 1);
    }, 1000);
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={delayAddOne}>+ 1</button>
    </div>
  );
}
Otra cosa interesante a tener en cuenta si echa un vistazo a la documentación de React es que si no se devuelve nada de esta función, entonces no se volverá a renderizar en absoluto.

Una vez que proporcionamos esta función interna a setState para obtener de manera confiable el estado anterior y devolver el nuevo estado de nuestra función, nuestro problema de estado obsoleto debido a nuestro closure desaparece.

Traducido del artículo de Reed Barger - What Every React Developer Should Know About State