Funcionamiento del estado en React
Cómo React gestiona el estado creado con useState en un Componente
React es la librería más popular para construir interfaces de usuario. Nos brinda la posibilidad de usar el mismo lenguaje (Javascript) para marcar y para programar.
Ademásm nos permite despreocuparnos de la gestión del estado — donde hay más complejidad — y de actualizar el trozo de vista que toque ante determinados cambios de estado, usando un algoritmo de diferenciación entre un DOM virtual y el real.
Así, en React no cambiamos nuestras vistas directamente, sino que lo hacemos a través de generar un nuevo estado que represente los cambios futuros. El estado se convierte en nuestra fuente de la verdad y nuestras vistas son un reflejo de ella.
Eso parece muy obvio hoy, pero si pensamos por ejemplo en como hacíamos interfaces de usuario hace algunos años, usando jQuery o vanilla, recordaremos que nuestra fuente de la verdad era el DOM y que en aras de no generar un nuevo árbol cada vez, mutábamos directamente los nodos cuando se producía una interacción del usuario o como resultado de algún proceso asíncrono. Es decir, mezclábamos en todo momento modelo y presentación, el cómo presentábamos era nuestro modelo.
Por si eso fuera poco, cualquier podía mutar el DOM (procesos asíncronos, librerías, etc.) y eso provocaba que nunca teníamos la certeza real de a qué momento del estado estábamos accediendo. A lo mejor queríamos ocultar un nodo que ya había sido ocultado por otro proceso, o peor, intentábamos modificar el valor de un input que había sido destruido por otro punto del programa.
Hoy no nos centraremos en las aplicaciones de useState, su sintaxis o variaciones, para eso tenéis la documentación oficial y tutoriales que exprimen bastante bien sus posibilidades.
Los seguidores habituales de esta newsletter sabrán que aquí nos interesamos más por el ‘qué’ que por el ‘cómo’, por lo que hoy hablaremos de qué es el estado en React y como realmente funciona dentro de nuestros componentes.
¿Qué es el estado?
Llamamos estado a la instantánea de una aplicación en un momento determinado. Es decir, si tenemos una interfaz como la siguiente:
La instantánea de esta vista representará todos los cambios que han ocurrido sobre esta desde su primer render. Así podríamos decir en lenguaje natural que el estado actual de esta pieza de UI es:
- Tenemos el valor 'aeinstein@me.com' escrito en el input nativo 'email'
- Tenemos el valor '1234' escrito sobre el input nativo 'password'Si después de esto, el usuario realiza nuevos cambios sobre la vista y preguntamos de nuevo por la instantánea del estado en ese momento, podríamos obtener:
- Tenemos el valor '' escrito sobre el input nativo 'email'
- Tenemos el valor '' escrito sobre el input nativo 'password'De esto podemos sacar que:
La vista es una representación del estado.
El estado cambia en función de una interacción de un actor (como el usuario).
Ante cambios en el estado, la vista cambia en consecuencia.
El estado está intrínsicamente relacionado con la vista.
Así podríamos decir que una representación del estado descrito anteriormente, podría tener esta pinta:
function Login({ email, password }) {
return (
<form>
<span>Email</span>
<input type="email" name=" value={email} />
<span>Password</span>
<input type="password" value={password} />
<input type="submit" value="Enviar" />
</form>
);
};O podría tener esta otra:
function Register({ email, password }) {
return (
<form>
<span>Email</span>
<input type="email" value={email} />
<span>Password</span>
<input type="password" value={password} />
<input type="submit" value="Register" />
</form>
);
};Ambos representan el mismo estado de forma distinta.
Decimos que el estado está relacionado con la vista, porque cambios en el estado generan cambios en la vista, e interacciones con la vista pueden generar cambios en el estado.
Es importante que la forma del estado sea agnóstica al cómo se pinta, así podremos tener la misma gestión de estado para un Login en la web, un Login en una terminal o un Login en un dispositivo móvil.
React es declarativo
Y más concretamente, el cómo creamos vista es declarativo. Es importante este matiz porque el usar React no implica que nuestro código se vuelva declarativo, sino que el definir la vista se hace de forma declarativa.
Pero, ¿qué es declarativo? En palabras resumidas una programación más imperativa contará con menos abstracciones y en cambio, una declarativa incorporará más abstracciones.
El ejemplo típico es pensar en cómo duplicar una lista de números. De forma imperativa tendremos algo parecido a esto:
const numbers = [1,2,3];
const res = [];
for(let i = 0; i < numbers.length; i++) {
res[i] = numbers[i] * 2;
}
console.log(res); // [2,4,6]En cambio, este mismo ejercicio resuelto de forma declarativa podría ser:
const numbers = [1,2,3];
const res = numbers.map(x => x * 2);En resumen:
En la forma imperativa estamos diciendo cómo son las cosas. Es decir, estamos cogiendo un array, recorriéndolo en un proceso iterativo y accediendo a cada valor mediante un índice, multiplicándolo y guardando el resultado en la posición correspondiente en un array de resultado.
En la forma declarativa estamos diciendo qué son las cosas. O lo que es lo mismo, estamos clamando que el duplicar una lista es transformar una existente en otra, donde cada elemento esté duplicado.
En el segundo caso nos centramos en el problema de dominio — duplicar una lista —, en el primero tenemos que definir primero como recorrer una lista, como acceder a sus miembros en un momento de la iteración determinado y como hacer la duplicidad.
Trasladado a React es sencillo. Si queremos cambiar el valor de un input.email con Javascript vanilla tendríamos que:
Buscar el elemento en el DOM — document.querySelector(‘input.email’) —.
Comprobar que exista y no hayamos obtenido un nulo.
Cambiar el value del input al nuevo valor
En cambio, con React, suponiendo que tenga representada mi vista con un componente y quiero cambiar el valor de input.email, solo tengo que actualizar el estado correspondiente a esa información. Todo el proceso de buscar elementos, validación y actualización es hecho por una abstracción y no tenemos que preocuparnos de nada.
Creando estado
Antes hemos definido el estado de la interfaz propuesta en lenguaje natural y aunque podría ser una representación válida, en React el estado se representa con variables con primitivas individuales o con objetos que aúnen varios elementos de la UI.
Es importante notar que la vista es una representación del estado y no al revés. Mientras más desacoplado esté la forma del estado de la vista, mucho mejor.
Recuerda, además, que el estado y la vista están relacionados por lo que si necesitamos pintar información en una página web que cambie mediante la interacción de diferentes actores, debemos definir esa información como estado.
La forma más sencilla de definir estado para un componente con React, es usando useState:
import { useState } from 'react';
function App () {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<Login email={email} password={password} />
);
}¿Por qué hemos declarado las propiedades email y password como estado aquí? Pues porque en función de una acción del usuario — escribir en los inputs — queremos que se renderice sobre los inputs lo que está escribiendo, por lo tanto, como tenemos una información que cambia en función de una interacción de un actor, debe ser estado.
De este modo, cuando se ejecuta useState por primera vez, este nos devuelve un array cuyo primer elemento puede ser una de dos:
O una copia, si el valor inicial es una primitiva (string, number, boolean, etc).
O la referencia a la no primitiva, si el valor inicial no es una primitiva (un objeto).
Esto es porque las primitivas en Javascript se pasan siempre por valor y las no primitivas por referencia. Así si por ejemplo el estado inicial para una pieza de estado fuese un Array de 100 elementos, el primer elemento del array que nos devuelve useState es ese mismo Array, no habiendo React efectuado ninguna copia:
const initialArray = [1,2];
const [value, setValue] = useState(initialArray);
value === initialArray // trueEl segundo elemento del array es una función que toma un valor y encadena un render para que el usuario vea por pantalla el resultado de actualizar el estado.
Habitualmente aquí usamos desestructuración para automáticamente crear variables nuevas para el valor del estado y la función de actualización. Esto no es React, es Javascript y podríamos hacer también esto:
const values = useState(‘‘);y después acceder individualmente al valor con values[0] y al set con values[1].
Si ejecutamos el ejemplo ahora, veremos que, al intentar escribir cualquier cosa en los inputs, no parece estar haciendo nada. Y es que es justo en este ejemplo donde podemos entender cómo funciona el estado.
Como no le hemos dicho a React cuándo y cómo actualizar el email y la password, lo único que vemos renderizando una y otra vez es el estado inicial — un string vacío —.
Actualizando estado
Aquí es cuando los conocimientos del API de React no son suficientes para entender completamente cómo funciona useState y las actualizaciones de estado que veremos a continuación.
Para que al escribir en uno de los inputs del formulario de Login planteado anteriormente se renderice lo que escribimos, necesitamos decirle a React que renderice el nuevo input con el valor una vez que este cambie. Sin embargo, como la forma en la que hacemos UI en React es declarativa, no tenemos que preocuparnos por renders ni nada por el estilo.
Lo único que tenemos que hacer es actualizar el valor del estado correspondiente, en nuestro caso sería actualizar las variables de estado email y password cuando el usuario produzca un evento change en cada uno de los inputs.
Vamos primero a habilitar estos eventos en el componente Login:
function Login({ email, password, onEmailChange, onPasswordChange }) {
return (
<form>
<span>Email</span>
<input type="email" value={email} onChange={onEmailChange} />
<span>Password</span>
<input type="password" value={password} onChange={onPasswordChange} />
</form>
);
}Aparte de las props de email y password, les estamos pasando una función callback para cada uno de los eventos change de cada uno de los inputs.
Ahora en nuestra función App — componente — podemos definir esos manejadores que se ejecutarán cuando el usuario escriba en cada input:
export default function App() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onEmailChange = (ev) => {
setEmail(ev.target.value);
};
const onPasswordChange = (ev) => {
setPassword(ev.target.value);
};
return (
<div className="App">
<Login
email={email}
password={password}
onEmailChange={onEmailChange}
onPasswordChange={onPasswordChange} />
</div>
);
Si ejecutamos el código ahora en el navegador, vemos que funciona todo correctamente y que, al escribir en cada input, vemos como la información se va visualizando.
¿Cómo se están actualizando las variables email y password si además de ser constantes, solo estamos llamando a useState una vez por cada una? La respuesta es que no se actualizan directamente.
Actualización en closure
Lo reconozco, soy muy pesado con los básicos y en una entrevista de trabajo me suelo enfocar más en el lenguaje en el cual están construidas las librerías que en las librerías en sí. Porque las documentaciones sabemos leerlas todos y hoy en día con la base adecuada podemos aprender cualquier librería en unas cuantas tardes, porque precisamente están diseñadas para que sean fáciles de usar.
React es una librería de Javascript, por tanto, se aprovecha de los mecanismos del lenguaje para implementar sus funcionalidades. Otra cosa es que lo haga de una forma más o menos transparente, pero nunca hará cosas que no se pueden hacer en Javascript de forma nativa.
Si intentamos hacer una interpretación de cómo está funcionando React sin conocer cosas tan elementales como un closure, nos encontraremos caminando hacía direcciones equivocadas y haciendo afirmaciones falsas como la de que las variables email y password se actualizan de forma asíncrona.
Es como si intento intuir cómo funciona una central nuclear sin haberme leído en mi vida un libro sobre el tema. La intuición sin una base de conocimiento es solo ignorancia motivada, lo cual es peligroso.
Vamos a hacer una prueba para intentar intuir con conocimiento que es lo que está ocurriendo, para ello vamos a añadir un console.log después de cada set de estado:
const onEmailChange = (ev) => {
setEmail(ev.target.value);
console.log(email);
};
const onPasswordChange = (ev) => {
setPassword(ev.target.value);
console.log(password);
};Ah, esto es interesante. Cada vez que escribamos en los inputs, el console.log imprimirá siempre el estado anterior a la actualización. Es decir, que si el estado inicial es una cadena vacía y escribimos en el input de email la letra ‘a’, el console.log imprimirá una cadena vacía.
¿Es qué setEmail no está actualizando el estado inmediatamente? O ¿acaso setEmail es asíncrono y por tanto hasta que no termine el proceso que haga no actualiza la variable email? Ninguna de las dos.
Primero hay que entender que tanto onEmailChange como onPasswordChange son closures y esto quiere decir que son funciones que se han de empaquetar con su lexical scope en el momento de su definición. Por ejemplo, el lexical scope de la función onEmailChange contiene referencias a la variable email y a la función setEmail, por lo que, aunque saquemos una referencia a la función fuera de su definición, vamos a poder acceder a esos elementos.
Podemos verlo en un ejemplo más concreto:
function Test () {
const name = 'Alba';
function closure() {
console.log(name);
}
return { fn: closure };
}
const myFunction = Test().fn;
myFunction(); // 'Alba';Aquí estamos ejecutando la función closure fuera del ámbito de definición y aun así podemos acceder a la información que referencia, puesto que se ha empaquetado con su lexical scope — en este caso, el acceso a la variable name —.
Volviendo a onEmailChange, el console.log hace referencia a la constante email definida en el ámbito del componente App, por lo que al ejecutar esta función tendrá acceso al valor que tuviese la constante cuando se declaró.
Es decir, si se declaró la constante email con el estado inicial (cadena vacía), ese será el valor que tendrá en todo el ciclo de vida de la función App. Si queremos cambiarlo, tendremos que volver a llamar a la función App recalculando el valor de la constante email.
Sea lo que sea que haga setCount no puede cambiar la constante definida cuando llamamos a useState, por lo que hacer count + 1 N veces dentro de un setCount, solo provoca que estemos haciendo setCount(0 + 1) muchas veces, puesto que la función onEmailChanged se empaquetó con la variable count cuando la función App fue ejecutada.
En segundo lugar, que las variables email y password no se actualicen inmediatamente después de llamar a sus sets, no implica que no estén haciendo nada. Y es que el hacer un set no implica un render automático ya que aquí es React el que decide cuando y como renderizar. Por tanto, el llamar a un set provoca que internamente React almacene el siguiente estado para una variable concreta en la instancia del componente.
En resumen, de aquí sacamos dos aprendizajes importantes:
Para que una función closure pueda acceder a nuevos valores, necesitamos volver a llamar a la función que define al closure para que de esta manera se redefinan las variables y de nuevo la función se empaqueté con el nuevo lexical scope.
Cuando hacemos set de una variable de estado, React internamente encola las actualizaciones para proceder a renderizarlas.
Re-Renderizando un componente
Para React, re-renderizar un componente porque se ha producido un cambio en el estado, no es otra cosa que volver a coger ese componente, volver a llamarlo, recrear las variables de estado con los nuevos valores y obtener el nuevo JSX actualizado que después le pasará a ReactDOM (en el caso de web) para que proceda a pintarlo.
De esta manera, si realizamos cambios en el email y la password a raíz de una acción del usuario, React cogerá la función componente donde estén definidas esas variables de estado, la volverá a llamar y encolará para renderizar el JSX resultante:
Después ReactDOM decidirá cuándo y cómo renderizar ese JSX en el DOM, pero eso es otra historia.
Lo importante es quedarse con que las variables de estado no son algo mágico que van sufriendo actualizaciones, ni que los sets son funciones asíncronas que van actualizando nuestro estado. Lo que realmente sucede es que un useState determinado está pegado a la instancia de un componente y mantiene todo el estado. Un set lo único que hace es almacenar el estado siguiente en un componente, para que cuando se vuelva a llamar al componente para renderizarlo, las variables de estado se actualicen en consecuencia.
Por eso muchas veces se habla de memorizar componentes o funciones porque al estar llamando a los componentes por cada actualización de estado, lo que estamos haciendo es redefinir una y otra vez las funciones que se encuentren dentro y eso en determinadas ocasiones puede interferir con el algoritmo de diferenciación que utiliza ReactDOM para saber que trozo del DOM actualizar.
Conclusión
Hemos visto que es el estado y cómo funciona a nivel de componente en React, además por el camino hemos repasado los conceptos sobre los que se sustenta la gestión del estado.
El conocer los conceptos sobre los que están edificados ciertos mecanismos, nos ayudará a no solo entender mejor que es lo que estamos haciendo sino a optimizar mucho mejor nuestros programas. Ya que si por ejemplo tenemos la conciencia de que nuestros componentes se van a ejecutar por cada render, seremos mucho más cuidadosos a la hora de definir utilidades u otras funciones a nivel de componente.
Lamentablemente me sigo encontrando muchas personas programadoras que se centran en aprender a usar APIs sin dedicar suficiente tiempo a afianzar los conceptos sobre los que se sustentan, llevándolos a desarrollar programas con bugs, difíciles de escalar y mantener.
Espero que este artículo junto con la gran cantidad de información y buenas prácticas sobre el uso de estado en React, contribuya a interiorizar bien los conceptos.
Nada más por hoy :).




