El patrón inicializador de estado
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 deinitial
(inicial) para estar en correspondencia con eldefaultValue
delinput
elemento. Aunque esto tiene sentido, yo todavía prefiero el prefijoinitial
debido a que siento queinitial
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):
Con el patrón inicializador de estado (aviso aquí hay animación):
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!!