Saltearse al contenido

Estados

Es importante mencionar que los componentes que hemos definido hasta el momento son componentes sin estado o stateless components. Esto quiere decir que no tienen ningún mecanismo propio que les permita alterar su representación en la interfaz de usuario, por lo que los valores con los que se inicializan son los que se mantienen durante toda la vida del componente.

Pese a ello, veremos que en la práctica, la mayoría de las aplicaciones web que desarrollemos necesitaran que los elementos que componen la interfaz de usuario puedan cambiar dinámicamente, ya sea por la interacción del usuario o por la respuesta de la aplicación a ciertos eventos. Para ello, React nos provee de un mecanismo que nos permite definir componentes con estado o stateful components. Estos componentes tienen la capacidad de modificar su representación en la interfaz de usuario en función de ciertos eventos que se producen en el mismo.

React emplea el término estado para referirse a cualquier dato que pueda cambiar durante la ejecución de una aplicación. Por ejemplo, el estado de un componente puede ser el valor de un campo de texto, el valor de un contador, el valor de un checkbox, etc. En general, el estado de un componente puede ser cualquier valor que pueda cambiar durante la ejecución de una aplicación.

Hooks

Para indicar un estado en un componente, React emplea un hook llamado useState, pero ¿qué es un hook?.

Los hooks son funciones que nos permiten agregar estados, manejar el ciclo de vida de los componentes, sincronizar componentes, manejar efectos secundarios, manejar contexto, etc. Los hooks sólo pueden ser empleados en componentes funcionales.

Estas funciones tienen ciertas características que las hacen muy útiles para el desarrollo de aplicaciones web:

  • Son declarativas. Esto quiere decir que nos permiten definir el estado de un componente en función de ciertos eventos que se producen en el mismo. De esta forma, no tenemos que preocuparnos por el cómo se debe actualizar el estado, sino que nos centramos en definir el estado que debe tener el componente en cada momento.

  • Son componibles. Esto quiere decir que podemos combinar varios hooks para definir el estado de un componente. De esta forma, podemos definir el estado de un componente en función de varios eventos que se producen en el mismo.

  • Son reutilizables. Esto quiere decir que podemos definir un hook y reutilizarlo en varios componentes. De esta forma, podemos definir el estado de varios componentes en función de los mismos eventos.

  • Son extensibles. Esto quiere decir que podemos definir nuestros propios hooks y utilizarlos en nuestros componentes. De esta forma, podemos definir el estado de nuestros componentes en función de eventos que se producen en ellos.

Existe una amplia variedad de hooks, e incluso podemos definir nuevos por nuestra cuenta. En esta materia, nos centraremos en los hooks más utilizados por la comunidad de React.

Antes de continuar, es importante mencionar ciertas reglas que debemos tener en cuenta al emplear hooks.

  • Los hooks deben llamarse en el nivel superior. Esto quiere decir que no se pueden llamar dentro de bucles, condiciones o funciones anidadas. Como mencionamos anteriormente, emplear un hook implica (en la mayoría de los casos) que un componente se vuelva a renderizar. Esto puede resultar particularmente problemático si llamamos a un hook dentro de un bucle, función anidada o estructura recursiva, ya que esto provocaría que el componente se renderizara en cada iteración o llamada. Por ello, los hooks deben llamarse en el nivel superior.

  • Los hooks deben llamarse desde componentes funcionales. Esto quiere decir que no se pueden llamar desde componentes de clase.

  • Los hooks siempre empiezan por la palabra use, seguido del nombre del hook. Esta es una regla mnemotécnica que nos permite identificar fácilmente los hooks predefinidos. Por ejemplo, veremos que el hook para manejar estados se llama useState.

  • Todos los hooks predefinidos se importan desde el módulo react.

useState

Recordemos que los estados son variables que se pueden modificar durante la ejecución de la aplicación y que, al hacerlo, provocan que el componente se vuelva a renderizar. Para definir un estado en un componente, React nos provee de un hook llamado useState.

El hook useState nos permite definir estados en componentes funcionales, típicamente utilizado para definir estados locales. Devuelve un array con dos elementos: el estado actual y una función que permite modificarlo. Además, recibe como parámetro el valor inicial del estado.

Para declarar este hook, se emplea la función useState seguida de paréntesis, indicando como argumento el valor inicial del estado que queremos definir.

Por otro lado, el resultado de la función useState se asigna a un arreglo empleando la sintaxis de desestructuración de arreglos.

Esto se debe a que, el hook useState devuelve un arreglo con dos elementos. El primer elemento representa el estado de un componente y el segundo elemento es una función que permite modificar dicho estado.

Este último punto es importante, ya que el hook useState devuelve un arreglo con dos elementos que serán asignados al primer y segundo elemento del arreglo, respectivamente. Como resultado, veremos que la declaración de un estado se realiza de la siguiente manera:

1
const [estado, setEstado] = use<NombreHook>(valorInicial);

Otro detalle a mencionar es que el nombre de la función que permite modificar el estado, por convención, empieza por la palabra set, seguido del nombre del estado.

Supongamos que queremos definir un contador que se incremente cada vez que el usuario haga clic en un botón. Es evidente, que el estado del componente será el valor del contador, el cual se inicializará a cero.

1
import React, { useState } from 'react';
2
3
function Contador() {
4
const [contador, setContador] = useState(0);
5
6
return (
7
<div>
8
<p>Contador: {contador}</p>
9
<button onClick={() => setContador(contador + 1)}>Incrementar</button>
10
</div>
11
);
12
};

A partir de este simple componente, podemos ver que la función setContador es invocada cada vez que el usuario hace clic en el botón. Esto provoca que el componente se vuelva a renderizar, pero esta vez con el valor del contador incrementado en uno.

Los hooks son asíncronos

Exiten ciertos aspectos que debemos tener en cuenta al emplear cualquier hook. Uno de los más importantes es que todos ellos se ejecutan de forma asíncrona.

Una tarea asíncrona es aquella que se ejecuta por fuera del hilo principal de ejecución. Esto quiere decir que, en el caso de setState, la función se ejecuta por fuera del ciclo de vida del componente.

Entonces, al invocar la función setState, el componente no se vuelve a renderizar inmediatamente, sino que se agrega a una cola de tareas, dentro de la cual pueden haber otras llamadas a otras funciones setState.

Esto quiere decir que si queremos modificar el estado de un componente en función de su estado actual, debemos emplear la función que recibe como parámetro el estado actual del componente. Veamos esto tomando como referencia el ejemplo del componente Contador que definimos anteriormente.

Claro que, utilizar exactamente el mismo componente no es la mejor forma de ilustrar este comportamiento, por lo cual alteraremos el componente. Lo que haremos será definir la función handleIncrementar dentro del componente. En esta función, invocaremos la función setContador dos veces, lo cual podría parecer que incrementará el contador en dos unidades. Sin embargo, veremos que esto no es así.

1
import { useState } from 'react';
2
3
function Contador() {
4
const [contador, setContador] = useState(0);
5
6
const handleIncrementar = () => {
7
setContador(contador + 1);
8
setContador(contador + 1);
9
};
10
11
return (
12
<div>
13
<p>Contador: {contador}</p>
14
<button onClick={handleIncrementar}>Incrementar</button>
15
</div>
16
);
17
};

Nótese que en este caso, la función handleIncrementar se ejecuta cada vez que el usuario hace clic en el botón, pero como anticipamos, el contador no se incrementa en dos unidades, sino en una.

La razón de esto es muy simple, dado que el estado contador y la ejecución de la función setContador tienen naturaleza síncrona y asíncrona, respectivamente. Por ello, al invocar la función setContador dos veces, el valor del contador no se actualiza inmediatamente.

Si analizamos más detenidamente el código, veremos que la función handleIncrementar invocará la función setContador con el mismo valor del contador, dado que la actualización del mismo es asíncrona y como sabemos, cualquier tarea asíncrona en JavaScript se ejecuta al concluir todas las tareas síncronas de su contexto de ejecución. Por ello, si hacemos múltiples cambios de un estado en un mismo evento, sólo se verá reflejado el último cambio.

Podemos notar esto si agregamos un console.log dentro de la función handleIncrementar.

1
const handleIncrementar = () => {
2
console.log("Estado antes de incrementar: ", contador);
3
setContador(contador + 1);
4
console.log("Estado después del primer incremento: ", contador);
5
setContador(contador + 1);
6
console.log("Estado después del segundo incremento: ", contador);
7
};

En palabras más simples, si un estado o conjunto estados debe actualizarse múltiples veces durante un mismo evento, necesitaremos controlar dicho estado (o estados) mediante otro mecanismo.

La solución más simple es utilizar una función flecha que reciba como parámetro el estado actual del componente y devuelva el nuevo estado.

1
const handleIncrementar = () => {
2
setContador((contadorActual) => contadorActual + 1);
3
setContador((contadorActual) => contadorActual + 1);
4
};

Este parámetro existirá únicamente en el contexto de la función flecha, por lo cual no es necesario declararlo en una variable.

La razón por la cual esto funciona así es que la función setContador, así como cualquier setter de estado, recibe como parámetro el estado actual del componente, el cual garantiza su actualización mediante la función flecha. Es decir, la función flecha gestiona las modificaciones del estado para un mismo evento.

En conclusión, si trabajamos con estados, lo recomendable es emplear funciones flecha que reciban como parámetro el estado actual del componente y devuelvan el nuevo estado. De esta forma, nos aseguramos de que el estado se actualice correctamente.

Manejo de estados con objetos

En ocasiones veremos la necesidad de controlar más de un estado en un componente. Ante esta situación, podemos optar por distintas estrategias. Por ejemplo, supongamos que queremos definir un componente para representar el perfil de un usuario, en el cual se muestre su nombre de usuario, su estado de conexión (activo o inactivo) y su bio (una descripción breve del usuario). No nos preocuparemos por el nombre de usuario, ya que este no cambiará. Sin embargo, los otros dos estados sí pueden cambiar.

Por un lado, podemos definir un estado que sea un objeto y que contenga todos los estados que necesitamos controlar. De esta forma, podemos modificar el estado de un componente en función de un evento que se produzca en el mismo.

1
import { useState } from 'react';
2
3
function Profile() {
4
const [user, setUser] = useState({
5
status: 'active',
6
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
7
});
8
9
return (
10
<div>
11
<p>Username: {user.username}</p>
12
<p>Status: {user.status}</p>
13
<p>Bio: {user.bio}</p>
14
</div>
15
);
16
};

Al hacerlo de esta forma, podemos modificar el estado del componente en función de un evento que se produzca en el mismo.

Para comenzar, supongamos que al hacer clic en el estado de conexión, queremos cambiar el estado de conexión del usuario. Es decir, si el usuario está activo, queremos cambiar su estado a inactivo y viceversa. Luego veremos como modificar el estado de la bio.

1
import { useState } from 'react';
2
3
function Profile() {
4
const [user, setUser] = useState({
5
status: 'active',
6
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
7
});
8
9
const handleStatusClick = () => {
10
setUser({
11
status: user.status === 'active' ? 'inactive' : 'active'
12
});
13
};
14
15
return (
16
<div>
17
<p>Username: {user.username}</p>
18
<p>Status: <span onClick={handleStatusClick}>{user.status}</span></p>
19
<p>Bio: {user.bio} </p>
20
</div>
21
);
22
};

Veremos que al hacer clic en el estado de conexión, el estado del usuario cambia de activo a inactivo y viceversa. Sin embargo, actualizar únicamente el estado de conexión, provoca que los otros estados se pierdan.

Esto se debe a que, al actualizar el estado del componente, se reemplaza el objeto que representa el estado actual por un nuevo objeto. Si el nuevo objeto no contiene todos los atributos del objeto anterior, esto es interpretado como si los atributos faltantes fueran undefined.

Una manera sencilla de solucionar esto es emplear el operador de propagación (...) para pasar el estado actual como parte del nuevo estado.

1
const handleStatusClick = () => {
2
setUser({
3
...user,
4
status: user.status === 'active' ? 'inactive' : 'active'
5
});
6
};

De esta forma, los atributos del objeto se mantiene intactos y sólo se modifica aquel que queremos cambiar. Al actualizar el estado del usuario, los atributos username y bio del objeto user se mantienen intactos y sólo se modifica el atributo status.

Como vemos, esta estrategia puede resultar útil en ciertos casos, pero puede volverse compleja de mantener si el componente tiene muchos estados. Después de todo, si sólo queremos modificar un estado, de igual manera debemos pasar todos los estados que no queremos modificar. Aun más importante, si queremos modificar otro estado, este requerirá de una nueva función. Por ejemplo, podríamos querer modificar la bio del usuario.

1
const handleBioClick = () => {
2
setUser({
3
...user,
4
bio: <textarea>{user.bio}</textarea>,
5
});
6
};

Con esto en mente, tener el siguiente razonamiento.

Si tendremos que definir una función para cada estado que queramos modificar, ¿por qué no trabajar con estados independientes?

En general, veremos que la mayoría de las aplicaciones web que desarrollemos tendrán estados independientes. Por ello, es recomendable definir un estado por cada elemento que queramos controlar en un componente.

1
import { useState } from 'react';
2
3
function Profile() {
4
const [status, setStatus] = useState('active');
5
const [bio, setBio] = useState('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
6
7
const handleStatusClick = () => {
8
setStatus(status === 'active' ? 'inactive' : 'active');
9
};
10
11
const handleBioClick = () => {
12
setBio(<textarea>{bio}</textarea>);
13
};
14
15
return (
16
<div>
17
<p>Username: {username}</p>
18
<p>Status: <span onClick={handleStatusClick}>{status}</span></p>
19
<p>Bio: <span onClick={handleBioClick}>{bio}</span></p>
20
</div>
21
);
22
};

Bibliografía