React ofrece una infinidad de ventajas a la hora de desarrollar la interfaz de una aplicación web mediante pequeños componentes que se pueden combinar. Sin embargo, incorpora algunos aspectos en su comportamiento que pueden resultar poco intuitivos e incluso sorprendentes para aquellas personas acostumbradas a la programación imperativa con JavaScript. Uno de los primeros obstáculos con el que se encuentran quienes comienzan sus andaduras con React suele ser la gestión adecuada del estado de los componentes.
En este post vamos a señalar tres fallos muy habituales que cometemos cuando no conocemos a fondo los mecanismos de React, y explicaremos por qué ocurren y cómo solucionarlos. Para ello, nos valdremos de un ejemplo de componente muy sencillo: el que implementa un interruptor para encender o apagar una lámpara inteligente.
La estructura básica de este componente tendría el siguiente aspecto:
function Lampara() {
const handleClick = () => {
// encender o apagar la lámpara
};
return <>
<p>La lámpara está {
// encendida o apagada
}</p>
<button onClick={handleClick}>Alternar</button>
</>
}
Error #1: Usar variables globales o externas como estado
Como has visto en el esqueleto de Lampara
, en React moderno, cada componente se escribe en forma de función. Estas funciones pueden recibir propiedades como parámetros, y devuelven como resultado un fragmento de la interfaz de la aplicación. Si queremos añadir algún tipo de interactividad al componente, necesitará disponer de estado y poder cambiar en función de este.
De cara a hacer funcional el interruptor de la lámpara, un primer acercamiento que podríamos intentar es utilizar una variable externa a nuestra función y mostrar el componente de una forma u otra según su valor. De esta forma, la implementación quedaría como la siguiente:
var encendida = false;
function Lampara() {
const handleClick = () => {
encendida = !encendida;
};
return <>
<p>La lámpara está {encendida ? "encendida" : "apagada"}</p>
<button onClick={handleClick}>Alternar</button>
</>
}
Como ves, tenemos una variable externa a la función Lampara
llamada encendida
que se inicializa a false
. Cuando se pulsa el botón, se invierte su valor.
Si ejecutas este componente y pruebas a pulsar el botón para "encender" la lámpara, verás que no funciona: el mensaje de encendido no llega a aparecer en el componente.
No te confundas: el evento se ha ejecutado y el valor de encendida
será ahora true
. Entonces, ¿por qué no cambia el mensaje? 🤔
La clave está en el mecanismo de actualización de React: cada componente se renderiza al mostrarse por primera vez y únicamente se vuelve a renderizar tras cambios en sus propiedades o su estado. A su vez, el estado de un componente funcional de React viene determinado por datos que se crean con la función useState
.
⚠️ En React, modificar directamente variables fuera del ámbito del componente no provoca una actualización del componente.
Por lo tanto, sería necesario reemplazar la variable global encendida
con un dato del estado llamado encendida
creado mediante la función useState
:
const [encendida, setEncendida] = useState(false);
Error #2: Mutar el estado directamente
Una vez que ya conocemos la forma de crear datos en el estado de un componente, es muy probable que caigamos en la tentación de modificarlos directamente, especialmente si se trata de objetos complejos como arrays.
Si has desarrollado componentes de React basados en clases, no será raro que hayas intentado modificar el estado de esta forma, por ejemplo, introduciendo un nuevo elemento en un array de la siguiente forma:
this.state.listaCompra.push(ingrediente)
Como resultado, habrás obtenido un componente que no reflejaba los datos más recientes.
⚠️ Peor aún: si has forzado un renderizado usando this.setState({})
, puedes "machacar" las mutaciones que hayas realizado previamente! 🤦🏻♂️
En nuestro componente funcional, sería muy fácil caer en el error de escribir la función gestora del evento "click" de esta forma:
function Lampara() {
let [encendida, setEncendida] = useState(false);
const handleClick = () => { encendida = !encendida; };
return <>
<p>La lámpara está {encendida ? "encendida" : "apagada"}</p>
<button onClick={handleClick}>Alternar</button>
</>
}
Efectivamente, tras hacer clic en el botón "Alternar" veremos que no ocurre nada.
Esto se debe a que, para que React tenga conocimiento del cambio y que se dispare una nueva renderización del componente, es necesario indicar el cambio mediante la función setEncendida
que nos devuelve useState
:
const handleClick = () => { setEncendida(!encendida); };
Error #3: No tratar actualizaciones de estado como asíncronas
Una vez que somos capaces de crear datos en el estado de nuestro componente y actualizarlos adecuadamente para que se reflejen los cambios en la interfaz, es posible que necesitemos esos datos para alguna otra tarea u operación que nuestra aplicación deba cumplir. Naturalmente, el dato encendida
se puede consultar directamente usando su identificador, pero haciéndolo así podremos tener problemas cuando actualicemos y consultemos un dato.
Imagina, por ejemplo, que queremos mostrar una notificación al pulsar el interruptor y alternar la lámpara. En este caso, por simplificar, imprimimos el mensaje en la consola con console.log
:
function Lampara() {
const [encendida, setEncendida] = useState(false);
const handleClick = () => {
setEncendida(!encendida);
console.log(`Acabas de ${encendida ? "encender" : "apagar"} la lámpara!`);
};
return <>
<p>La lámpara está {encendida ? "encendida" : "apagada"}</p>
<button onClick={handleClick}>Alternar</button>
</>
}
Verás que ocurre algo extraño: cuando la lámpara está apagada y pulsamos el botón por primera vez, el texto del componente cambiará a "La lámpara está encendida", pero el mensaje que aparece en la consola dirá lo contrario: "Acabas de apagar la lámpara".
¿Cómo es posible, si hemos modificado correctamente el dato encendida
del estado del componente? 🤔
Tienes que tener en cuenta que las actualizaciones de React son asíncronas: cuando llamamos a setEncendida(!encendida)
, React programa la actualización del estado, pero no ocurre inmediatamente. Por lo tanto, cuando intentamos acceder a encendida
, desde la línea siguiente para imprimir el mensaje en la consola todavía obtendremos el valor anterior del estado.
Las actualizaciones en React funcionan de esta forma por varios motivos:
- Mantener la inmutabilidad de los datos a lo largo de la renderización de un componente, lo que ayuda a que el código esté libre de errores y sea fácil de mantener.
- Acumular actualizaciones, si uno o más eventos modifican varios datos, de modo que el componente se vuelva a renderizar una sola vez, con todos los cambios que haya habido, en lugar de tener que renderizarlo varias veces.
Conclusiones
Acabamos de dar un repaso a los tres errores más típicos en los que podemos caer cuando trabajamos con el estado de un componente en React. Estos fallos eran aún más comunes cuando los componentes se desarrollaban en forma de clases, pero en la actualidad siguen siendo una fuente de problemas ya que no es la forma de programar más familiar.
¡Espero que te sea útil en tus proyectos con React y que prestes atención para no cometerlos! 😉