¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Gestión de eventos y manejo del estado
Notas del curso Desarrollo de aplicaciones web con React 18
Una parte crucial de toda aplicación web es la interactividad, es decir, la capacidad para recoger información de la persona visitante y reaccionar, mostrando los resultados convenientes. Esta capacidad realmente se puede desglosar en dos aspectos complementarios:
- La habilidad de manejar eventos: cuando se hace clic sobre un botón, se rellena un campo de texto o se pasa el cursor por encima de un menú.
- La manipulación del estado de la aplicación: en el momento en que un dato se modifica, la actualización debe verse reflejada en todos los lugares donde se muestre o tenga efecto.
Gestión de eventos
Hasta ahora hemos estudiado principalmente cómo mostrar información de diversa índole en la interfaz generada por un componente. Un aspecto importante a la hora de hacer interactiva esta interfaz es que tenga elementos manipulables por la persona visitante. En esta lección introduciremos los mecanismos por los que nuestra aplicación será capaz de responder a diferentes eventos.
Asociación de eventos a elementos
Aunque las etiquetas HTML estándares proporcionan atributos relativos a eventos como onclick, onchange u onsubmit, es poco común su uso salvo en aplicaciones muy pequeñas o gestores de eventos que consistan en una línea de código. Lo más habitual es asociar los eventos a las funciones dentro del propio código JavaScript, por ejemplo mediante addEventListener.
Al escribir código con React, la definición de la interfaz pasa a ser parte de la lógica de la aplicación, por lo que tiene más sentido que volvamos a asociar los eventos a los elementos, en este caso, mediante las propiedades onClick, onChange, onSubmit, etc. Estas propiedades deben tener como valor una función, a diferencia de los atributos HTML clásicos en los que se suele introducir una llamada o directamente código JavaScript.
Por ejemplo, el siguiente código HTML consiste en un botón que al pulsarlo lanzaría una ventana de confirmación y otro que cierra un hipotético diálogo:
<button onclick="confirm('¿Seguro?')">Aceptar</button> <button onclick="cerrarDialogo()">Cancelar</button>
El equivalente en React tendría este aspecto:
<button onClick={() => confirm('¿Seguro?')}>Aceptar</button> <button onClick={cerrarDialogo}>Cancelar</button>
En apariencia guardan bastantes similitudes, pero hay que tener en cuenta que en React al asociar una función simplemente la nombramos (sin paréntesis). Y si queremos escribir código directamente, tenemos que encuadrarlo en una función anónima.
Funciones gestoras de eventos
Dentro de un componente podemos declarar funciones que se invoquen cuando suceda un evento en la página. Estas funciones, a diferencia de la función correspondiente al componente, sí pueden tener efectos secundarios. Eso quiere decir que, para responder a un evento, podemos acceder o transmitir información a un almacén externo, realizar solicitudes a APIs, leer archivos, etc.
Volviendo a nuestra aplicación de lista de tareas, vamos a asociar una función eliminarTarea al evento onClick del botón de eliminar:
{completada && <button onClick={eliminarTarea}>Eliminar</button> }
Para que la función pueda ejecutarse, la podemos definir en el cuerpo del componente Tarea:
const Tarea = ({ titulo, completada }) => { const eliminarTarea = (event) => { // acciones para eliminar una tarea... console.log(`Tarea "${titulo}" eliminada.`) } // resto del componente }
Puedes observar que eliminarTarea recibe como parámetro un objeto event nativo de JavaScript. En muchas ocasiones podemos omitirlo, si no lo necesitamos, para manipular la forma en que el navegador propaga o responde a los eventos. Pero habrá casos en los que necesitemos controlarlo.
Por ejemplo, imagina que tenemos un formulario para añadir una nueva tarea a la lista:
const FormularioNueva = () => { return ( <form onSubmit={agregarTarea}> <input type="text" name="titulo" placeholder="Nueva tarea" /> </form> ) }
En este caso, enviar el formulario supondría por defecto un refresco de la página. Para evitarlo, recurriremos al método preventDefault del objeto event, que bloqueará los efectos del envío:
const agregarTarea = (event) => { event.preventDefault() // código para añadir la tarea al almacén console.log("Nueva tarea añadida") }
El objeto event también permite a la función que gestione el evento acceder al elemento que lo ha disparado mediante su propiedad target. De este modo podemos obtener datos, como por ejemplo el texto introducido en un campo.
Por ejemplo, mostramos en consola el título seleccionado para la nueva tarea cada vez que sufra un cambio:
const FormularioNueva = () => { return ( <form onSubmit={agregarTarea}> <input type="text" name="titulo" placeholder="Nueva tarea" onChange={event => console.log(event.target.value)} /> </form> ) }
En las próximas lecciones conoceremos los mecanismos para almacenar datos introducidos mediante eventos y modificar la información que muestra la aplicación, de forma que haremos posible el procesamiento de formularios mediante React.
Gestión del estado con hooks
Hasta ahora hemos estado escribiendo componentes cuyas propiedades representan un estado inmutable, puesto que no se pueden modificar en el código del componente. Con ello hemos aprendido a mostrar información a partir de trozos de código reutilizables, pero por ahora no tenemos forma de modificar dicha información. En este módulo vamos a añadir la funcionalidad necesaria para que el estado de los componentes cambie y, por tanto, podamos añadir, modificar y eliminar instancias de estos en función de lo que haya que mostrar en la página.
Qué es un hook
Los hooks son una funcionalidad que se introdujo en React 16.8 para permitir separar y reutilizar partes de la lógica de los componentes. Por ejemplo, para acceder a información en un almacén externo o controlar los datos de sesión. Además, extienden enormemente la forma moderna de desarrollar componentes mediante funciones, ya que antes de los hooks estos no podían tener un estado propio.
Los hooks son, por tanto, funciones que permiten la creación y el acceso al estado y a los ciclos de vida de React.
Una particularidad a tener en cuenta es que los hooks solo se pueden invocar en el cuerpo de la función de un componente (o en otro hook) y esas llamadas deben estar fuera de cualquier estructura de control como condicionales, bucles o funciones anidadas.
El hook ''useState''
Aunque React implementa de serie varios hooks útiles, nos vamos a centrar en useState, que es el que nos permite escribir componentes con un estado que puede cambiar. Primero, veamos un ejemplo sencillo de uso y a continuación explicaremos todos los detalles.
Imagina que estamos desarrollando una aplicación para domótica y queremos controlar el estado de un interruptor que enciende y apaga una bombilla inteligente. Podremos construir un componente Interruptor como el siguiente:
const Interruptor = ({ estadoInicial = true }) => { const [encendido, setEncendido] = React.useState(estadoInicial) return <> Estado actual: {encendido ? "ON" : "OFF"} <button>{encendido ? "Apagar" : "Encender"}</button> </> }
Fíjate primero en la segunda línea del listado. En ella se realiza una llamada al hook useState, proporcionándole un estado inicial para el dato encendido, y nos devuelve dos elementos: por un lado, el propio estado y, por otro, una función que servirá para modificarlo.
Para que React pueda llevar un registro de los cambios del estado y actualizar el componente acordemente, será necesario que siempre recurramos a la función devuelta para modificarlo, cuya referencia hemos guardado en la constante setEncendido.
Un detalle que puede resultar chocante es que recibamos estos valores en constantes y no variables. ¿Cómo es posible que un valor que vaya a cambiar lo tengamos asignado a una constante? La respuesta es sencilla: el valor de encendido se mantiene durante toda la ejecución de Interruptor y, si en algún momento lo modificamos, lo haremos mediante la función setEncendido que nos ha proporcionado el hook, lo cual lanzará una nueva ejecución del componente donde encendido tendrá el nuevo valor que le corresponda. De esta forma se mantiene la inmutabilidad de los valores con los que trabajamos.
Por lo demás, podemos hacer uso de la constante encendido como si fuera otra propiedad más. En este caso, como se trata de un valor booleano, lo utilizamos para renderizar un indicador de si la lámpara está encendida o no, y un botón que permite apagarla o encenderla según corresponda.
Modificando el estado
Para que el botón sea capaz de apagar o encender la supuesta lámpara inteligente, tendremos que añadir un gestor del evento onClick:
const Interruptor = ({ estadoInicial = true }) => { const [encendido, setEncendido] = React.useState(estadoInicial) const cambiarEstado = () => { setEncendido(!encendido) } return <> Estado actual: {encendido ? "ON" : "OFF"} <button onClick={cambiarEstado}>{encendido ? "Apagar" : "Encender"}</button> </> }
La pequeña función que hemos añadido previo a renderizar el componente establece un nuevo valor para encendido mediante la función auxiliar setEncendido. Se ejecutará cuando se pulse el botón de forma que React reciba el nuevo valor y genere una nueva renderización del componente utilizando el nuevo valor.
Es muy importante que la función que gestiona el evento no modifique directamente el valor de encendido, ya que React no podrá detectarlo y actualizar el componente. Sin embargo, dicha función sí puede tener otros efectos secundarios, como llamadas a una API.
Hooks personalizados
Al igual que podemos utilizar useState para controlar variables cuyos valores podemos alterar a voluntad, también podemos crear nuestros propios hooks en base a useState y otros. Por ejemplo, si la hipotética aplicación de control de aparatos domóticos tuviera varios componentes que utilizasen variables booleanas, podríamos escribir un hook useToggle que permitiría simplificar el código, encapsulando el comportamiento de “activación” y “desactivación”:
const useToggle = (estadoInicial = false) => { const [valor, setValor] = React.useState(estadoInicial) const alternar = () => setValor(!valor) return [valor, alternar] }
La lógica de alternar es la misma que la de la función cambiarEstado que teníamos en el componente Interruptor. Ahora, la hemos abstraído en este hook para poder reutilizar el comportamiento y simplificar el componente. Este quedaría ahora mucho más escueto, como puedes ver a continuación:
const Interruptor = ({ estadoInicial = true }) => { const [encendido, cambiarEstado] = useToggle(estadoInicial) return <> Estado actual: {encendido ? "ON" : "OFF"} <button onClick={cambiarEstado}>{encendido ? "Apagar" : "Encender"}</button> </> }
Otros hooks de React
Aunque el hook principal que vamos a utilizar a lo largo de este módulo es useState, conviene que conozcas la existencia y el propósito de los otros hooks que incorpora React:
useEffect: permite establecer una serie de efectos secundarios (p.ej. llamadas a APIs, cambios en el título de la ventana) que no se podrían ejecutar directamente en el cuerpo del componente.useContext: facilita el acceso a variables de estado global, como el tema de color de la aplicación o la cuenta de usuario que tiene sesión iniciada.useReducer: permite a varios componentes consultar y gestionar una misma sección del estado de nuestra aplicación utilizando un mecanismo de reducers y acciones que estudiaremos más adelante con Redux.useRef: permite capturar un nodo del DOM en una variable, si necesitamos utilizar directamente algún método del navegador, por ejemplo, para poner el foco en un campo o pausar un medio en reproducción.useMemo: memoriza resultados de cálculos potencialmente costosos para evitar repetirlos en las actualizaciones de la interfaz.useCallback: similar a useMemo pero para definir funciones y que la misma definición se mantenga entre actualizaciones, sin redefinirlas cada vez.
Existen algunos hooks más pero de uso muy limitado, los puedes consultar en la documentación oficial.
Componentes controlados en formularios
Los campos que se utilizan en HTML para recibir entradas por parte de las personas visitantes suelen mantener un estado interno, y lo actualizan en base a las interacciones que sufran. En React, sin embargo, nos interesa tener un acceso más directo a dicho estado, para poder consultarlo y manipularlo según convenga. Esto se consigue mediante un mecanismo que denominan “componentes controlados”, lo que quiere decir que el estado del componente (ya sea un campo de texto, una casilla de verificación o una lista de selección) está permanentemente vinculado con una propiedad del estado de React.
Esqueleto de un componente controlado
Un componente controlado debe contar con:
- Un elemento que reciba la entrada
- Una propiedad en el estado (creada con
useState) - Un gestor del evento
onChange
Por ejemplo, para que el formulario de añadir nueva tarea sea funcional, debemos controlar el campo de texto que da título a la tarea:
const FormularioNueva = () => { const [nuevaTitulo, setNuevaTitulo] = useState("") // ... return ( <form onSubmit={manejarSubmit}> <input type="text" name="titulo" placeholder="Nueva tarea" value={nuevaTitulo} onChange={event => setNuevaTitulo(event.target.value)} /> </form> ) }
Como puedes observar, hemos combinado la constante nuevaTitulo (que mantendrá el estado del título de la nueva tarea) con el gestor de evento en línea que se muestra en la propiedad onChange del elemento input. Si accedes a la aplicación en este momento, parecerá que el campo de texto se comporta con normalidad, pero en las herramientas de desarrollo de React podrás comprobar que ahora se muestra su valor en el estado del componente FormularioNueva
La ventaja de haber controlado el campo es que ahora, al enviar el formulario para añadir una nueva tarea, dispondremos de su título en la constante nuevaTitulo. Se puede decir que está vinculada al campo de texto, y no tenemos ya necesidad de trabajar con el DOM al gestionar el envío de datos del formulario: ¡tenemos la información que necesitamos en el propio componente!
