Actualizado 22-12-2021 | 6 min de lectura

El patrón inicializador de estado

white abstract geometric artwork

Traducido del original The State Initializer Pattern creado por Kent C. Dodds.

Imagen de Silvio Kundt

Cuando estaba trabajando en downshift, me encontré con una situación donde mis usuarios (y yo también) necesitaban la habilidad de poder reiniciar el dropdown, que estábamos programando, en cualquier momento a su estado inicial: ningún valor de entrada (no input value), nada destacado (highlighted), nada seleccionado, cerrado. Pero también habían usuarios que querían que el "estado inicial" tuviera por defecto algún valor, alguna selección, o permaneciera abierto. Entonces se me ocurrió el patrón inicializador de estado para dar soporte a esos casos de uso.

El patrón inicializador de estado permite exponer una API para que los usuarios puedan reiniciar tus componentes a su estado original sin tener que desmontar y montar completamente el componente.

Realmente, este patrón es similar en varias maneras a defaultValue en HTML. Algunas veces el consumidor de tu hook o componente quiere inicializar el valor de tu estado. El patrón inicializador de estado te permite hacer eso.

Mira este ejemplo:

function Counter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);
  const reset = () => setCount(0);
  return (
    <div>
      <button onClick={increment}>{count}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

El componente tiene una manera de inicializar su estado (a 0) y también permite reiniciar el estado a ese valor inicial.

Entonces el propósito de este patrón es el de permitir, a usuarios externos de nuestro componente, controlar el valor inicial del estado. Por ejemplo. Si alguien quisiera iniciar un contador en 1, se pudiera hacer lo siguiente:

<Counter initialCount={1} />

Algunas librerías que implementan este patrón usan el prefijo default (defecto) en vez de initial (inicial) para estar en correspondencia con el defaultValue del input elemento. Aunque esto tiene sentido, yo todavía prefiero el prefijo initial debido a que siento que initial comunica el propósito y caso de uso con más claridad.

El siguiente código ilustra lo necesario para dar soporte a la propiedad initialCount (initialCount prop):

function Counter({ initialCount = 0 }: { initialCount?: number }) {
  //              ^^^ acepta la propiedad `initialCount` con un valor por defecto por lo que es opcional
  const [count, setCount] = React.useState(initialCount); // <-- pasa esta al estado
  const increment = () => setCount((c) => c + 1);
  const reset = () => setCount(initialCount); // <-- pasa el valor de initialCount a la función de reinicio (reset)
  return (
    <div>
      <button onClick={increment}>{count}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Y aquí está con un contador inicial igual a 8:

Ese es el núcleo del patrón. Pero le falta un importante caso extremo.

¿Qué sucedería si el usuario de tu componente cambia el valor de initialCount después que tu componente está montado? Invalidaría esto el propósito de toda la parte initial (inicial) del nombre de la propiedad? Aquí está un ejemplo donde el consumidor de nuestro contador cambia initialCount cada 500 milisegundos luego de haberse montado inicialmente (initial mount).

Haciendo clic en el botón "reset" (reiniciar) reiniciará nuestro componente en un estado diferente a su estado inicial, lo que es probablemente un error, por lo que queremos que esto no sea posible. Haz clic varias veces y este se reiniciará con un valor completamente diferente cada vez. Claro, estoy de acuerdo contigo. Este es un ejemplo de alguien usando la API incorrectamente. Pero como no es mucho trabajo, probablemente estaríamos de acuerdo en hacer esto imposible.

Entonces, cómo podríamos agarrar el valor inicial real e ignorar cualquier cambio a esta propiedad? Tengo un indicio para ti. No es tan complicado como un useEffect con un isMounted booleano (isMounted boolean) o algo parecido. Es realmente muy simple. Y existen varias formas de lograrlo:

const { current: initialState } = React.useRef({ count: initialCount });
const [initialState] = React.useState({ count: initialCount });
const [initialState] = React.useReducer((s) => s, { count: initialCount });

// real contador inicial es: initialState.count

De esas opciones prefiero useRef, pero seguramente usted tendrá su propia preferencia sobre esto. Manos a la obra! Aquí está con initialCount igual a 2:

Incluso si alguien fuera a cambiar aleatoriamente el valor de initialCount, nuestro componente no se afectaría.

Reiniciando estado mediante key

Si no haz leído Comprendiendo la propiedad 'key' de React (Understanding React's key prop), te recomiendo que le des una rápida lectura antes de continuar.

Otra cosa que quisiera resaltar es que realmente puedes reiniciar un componente muy fácil sin necesidad de una API específicamente para ello. Existe una API embebida en React para todos los componentes: la propiedad key. Simplemente proporcionas una key y estableces esa propiedad key con un nuevo valor cada vez que quieras reiniciar el componente. Esto desmontará y montará el componente como nuevo. Compruébalo en el siguiente código:

function KeyPropReset() {
  const [key, setKey] = React.useState(0);
  const resetCounter = () => setKey((k) => k + 1);
  return <KeyPropResetCounter key={key} reset={resetCounter} />;
}

function KeyPropResetCounter({ reset }) {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);
  return <CountUI count={count} increment={increment} reset={reset} />;
}

Y aquí está renderizado:

Te habrás dado cuenta que tuvimos que reestructurar un poco el código para lograr esto. Sin embargo en algunas situaciones esto puede no ser posible o deseable.

Adicionalmente, desmontar y montar el componente nuevamente (que es lo que la propiedad key hará) tiene más implicaciones. Por ejemplo en mi taller Patrones Avanzados de React (Advanced React Patterns), tenemos una animación cuando el estado cambia. Mira el impacto de la propiedad key en esto:

Con el enfoque reinicio por key (aviso no hay animación):

Haciendo clic en el componente `toggle`, luego en el botón de reiniciar (reset) no muestra animación por reiniciar

Con el patrón inicializador de estado (aviso aquí hay animación):

Haciendo clic en el componente `toggle`, luego en el botón de reiniciar (reset) no muestra animación por reiniciar

También desmontar y remontar componentes hará llamadas a la función de limpiezas de useEffect (useEffect cleanups) y a callbacks. Puede que sea eso lo que quieres, pero puede que no lo sea.

Conclusión

El patrón inicializador de estado es muy simple. De hecho, lo omití durante un largo período de mi taller Patrones Avanzados de React (Advanced React Patterns) porque pensaba que no merecía dedicarle tiempo. Pero durante varios talleres sin este la gente me preguntaba por cuestiones que este resuelve, por lo que decidí añadirlo. Espero que este artículo te ayude con tu trabajo. Buena suerte!!

Manténme actualizado

Creado por Yampier Medina

© 2020 Licencia MIT