¡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!
Transmitiendo el estado entre componentes
Una vez que conoces los mecanismos para mantener y modificar un estado en la aplicación, es conveniente ser capaz de comunicar tanto el valor actual del estado como los cambios que ocurran de unos componentes a otros.
Es poco conveniente tener el estado duplicado o repetido en varios componentes, ya que implica mucha comunicación para actualizar el estado de forma que sea consistente en todos los lugares. En vez de eso, lo ideal es que tengamos un solo sitio de referencia para cada dato y ese valor se comunique al resto de componentes que lo necesiten. De esta forma, la comunicación de ese estado fluye únicamente en una dirección.
Comunicando el estado a componentes descendientes
La forma más habitual de indicar un dato a un componente descendiente es proporcionarlo a través de una propiedad.
Por ejemplo, el dato más relevante de la aplicación, que debe tener estado y al que otros componentes accederán, es la lista con las tareas. Para facilitarlo vamos a utilizar el hook useState de modo que la lista pueda modificarse. Esto nos permitirá, más adelante, escribir funciones que actualicen la lista cuando desde la interfaz se añadan, borren o editen tareas.
// en el componente App const [tareas, setTareas] = useState([ { titulo: "Aprender componentes de React", completada: false }, { titulo: "Completar las prácticas del módulo 1", completada: true }, { titulo: "Realizar la autoevaluación", completada: false } ])
El resto de componentes que creemos dentro de App pueden recibir tanto la información del objeto tareas, parcial o completa, como la función de actualización setTareas. Por ejemplo, tenemos el componente ListaTareas que renderiza la lista en sí, o no renderiza nada si no hay tareas:
// en el componente App <ListaTareas tareas={tareas} />
Al pasar tareas como una propiedad del mismo nombre, podemos recurrir a desestructuración del objeto de propiedades en el componente descendiente y llamarla de la misma forma. Esto nos permite ser consistentes con los nombres de los objetos que provienen de otros componentes, con la ventaja añadida de que podemos utilizar esta estrategia para descomponer un componente complejo en varios componentes más pequeños:
const ListaTareas = ({ tareas }) => { if (tareas.length == 0) { return null; } return ( <> <ul> {tareas.map((tarea, i) => <Tarea {...tarea} key={i} />)} </ul> </> ) }
Como puedes observar, la forma más sencilla de hacer que el estado de un componente esté disponible para otro es colocarlo en el nodo padre y transmitirlo mediante una o varias propiedades al hijo. Si en el componente App se modifica el valor de la lista de tareas, los componentes descendientes que la reciban como propiedad serán actualizados y se volverán a renderizar de forma automática.
Comunicando el estado a componentes ascendientes
Las propiedades de React solamente fluyen de nodos padre a nodos hijo. Por tanto, para indicar valores de estado de hijos a padres, necesitamos una nueva estrategia.
Volvamos a nuestro formulario para añadir una tarea a la lista. El componente que hemos desarrollado tiene un valor nuevaTitulo que se convertirá en el título de una tarea cuando el formulario se envíe:
const FormularioNueva = () => { const [nuevaTitulo, setNuevaTitulo] = useState("") // ... }
Sin embargo, la lista de tareas está almacenada en el componente App, por lo que necesitaremos transmitirle este dato cuando ocurra el evento.
Una forma de conseguirlo es componer una función en el componente App que nos permita actualizar la lista de tareas, y proporcionarla al formulario como una propiedad:
// en el componente App const agregarTarea = (titulo) => { const nueva = {titulo, completada: false} setTareas([...tareas, nueva]) }
Recuerda que, en este contexto, la sintaxis {titulo} equivale a {titulo: titulo}.
Observa que, para añadir una nueva tarea a la lista, no podemos llamar al método push() del vector tareas porque estaríamos modificando el estado directamente. En su lugar, creamos un nuevo vector que contenga todas las tareas previas (…tareas, operador spread) y la nueva al final, y lo pasamos como parámetro a la función setTareas (obtenida del hook useState) que se encargará de modificar la lista.
Ahora que disponemos de esta función, añadiremos una propiedad al componente FormularioNueva para poder llamarla cuando se envíe el nombre de una nueva tarea:
// en el componente App <FormularioNueva agregar={agregarTarea} />
En el componente, recibimos esta propiedad en los parámetros y la utilizamos como función para comunicar el título de la nueva tarea, dentro del gestor del evento submit del formulario:
const FormularioNueva = ({ agregar }) => { const [nuevaTitulo, setNuevaTitulo] = useState("") const manejarSubmit = (event) => { event.preventDefault() agregar(nuevaTitulo) setNuevaTitulo("") } // ...
Las tareas que realiza el gestor del evento, por tanto, son las siguientes:
- Evita el refresco de la página deteniendo el efecto habitual del evento mediante
preventDefault(). - Transmite el contenido de la nueva tarea y delega la gestión de la estructura de datos a la función dada por la propiedad
agregar. - Restablece el contenido del campo de texto para que esté listo para agregar otra tarea si se desea.
Uso del contexto para mantener estado global
Como ya tienes claro, la manera principal de definir variables con estado y gestionarlas es a través del hook useState dentro de algún componente, ya que conviene que cada componente se encargue de gestionar sus propios datos y solo tengan acceso a ellos los componentes que realmente lo necesiten. Sin embargo, existe una forma alternativa de proporcionar un estado más global a la aplicación, al que pueda acceder cualquier componente. Se denomina contexto, y nos permitirá manejar datos relativamente simples a los que pueden necesitar acceso componentes muy diferentes.
Elementos de un contexto
Para crear y utilizar un contexto necesitamos los siguientes tres elementos:
- Una constante, donde se almacenará una referencia al contexto creado con un método específico de React llamado
createContext(). Generalmente el nombre de esta constante se prefijará con la palabraContextoo, en inglés, se le añade el sufijoContexten el nombre. Por ejemplo, si creásemos un contexto global para almacenar el tema (claro u oscuro) que se aplicará a la interfaz de nuestra aplicación, lo denominaríamosContextoTemaoThemeContext. - Utilizar un componente
<NombreContexto.Provider>, que nos proporcionará React automáticamente, para poder proporcionarle un valor al contexto. En nuestro ejemplo del tema, el componente se llamaría<ContextoTema.Provider>(o bien<ThemeContext.Provider>en inglés), y tendría una propiedadvalueque contendría el valor del tema. - Realizar una llamada al hook
useContext(ContextoTema)en los componentes donde queramos acceder al valor.
La constante en la que se almacena la referencia al contexto se debe definir fuera del componente donde se utiliza. De otro modo, si se definiese dentro, se estaría creando un nuevo contexto cada vez que se renderizase el componente. Por tanto, la constante debe estar en el ámbito global del módulo, junto a los imports y las funciones.
En los siguientes apartados, crearemos paso a paso un contexto para configurar un tema de color en nuestra aplicación.
Creación del contexto
Vamos a comenzar importando las funciones que necesitaremos para crear y obtener el contexto. En las primeras líneas del componente raíz App.jsx, junto al resto de imports, incluiremos:
import { createContext, useContext } from 'react'
Un contexto puede contener cualquier cosa: un valor simple, un array o un objeto. En nuestro caso, vamos a definir un par de temas de color para poder dar un aspecto más personalizado a la lista de tareas:
const temas = { claro: { "texto": "#3c3b3d", "fondo": "#eff1f5", "fondo2": "#ccd0da" }, oscuro: { "texto": "#cdd6f4", "fondo": "#1e1e2e", "fondo2": "#313244" } }
Ahora, podemos crear el contexto y, opcionalmente, darle un valor por defecto:
const ContextoTema = createContext(temas.claro);
Definición del componente del contexto
El contexto que hayamos creado será accesible desde los nodos hijos del componente derivado del mismo (el “Provider” que se genera automáticamente al definir el contexto). Por este motivo debemos tener cuidado a la hora de colocarlo, para que todos los componentes que puedan necesitar cambiar de color en función del tema lo puedan consultar.
En nuestro caso, vamos a incluir el componente proveedor del contexto en el componente principal App:
function App() { // ... return ( <ContextoTema.Provider value={temas.claro}> <div className="App"> <h1>Kanpus</h1> <Cabecera tareas={tareas} /> <ListaTareas tareas={tareas} /> <FormularioNueva agregar={agregarTarea} /> </div> </ContextoTema.Provider> ) }
Como ves, facilitar el contexto es tan simple como envolver toda la interfaz en el componente derivado del contexto: <ContextoTema.Provider>. Es obligatorio asignarle un valor con la propiedad value, ya que el valor por defecto que le hemos asociado inicialmente solo estará disponible para componentes que no tengan un nodo ascendiente de este tipo.
El contexto que hemos creado está ya accesible desde los componentes Cabecera, ListaTareas y FormularioNueva, así como todos sus descendientes. Sin embargo, el valor del tema es totalmente fijo, no se puede modificar. Para permitir que la persona visitante alterne entre el tema claro y el oscuro, vamos a añadir un nuevo valor de estado tema que almacene el nombre del tema que se está utilizando. Dos nuevos botones permitirán activar cada uno de los temas, siendo visibles únicamente cuando esté activo el tema opuesto:
function App() { // ... const [tema, setTema] = useState("claro") return ( <ContextoTema.Provider value={temas[tema]}> <div className="App"> <h1>Kanpus</h1> <Cabecera tareas={tareas} /> <ListaTareas tareas={tareas} /> <FormularioNueva agregar={agregarTarea} /> {tema == "claro" ? <button onClick={() => setTema("oscuro")}>Activar tema oscuro</button> : <button onClick={() => setTema("claro")}>Activar tema claro</button> } </div> </ContextoTema.Provider> ) }
Consulta del valor del contexto
Para que los colores del tema se apliquen a los elementos de nuestra aplicación, tenemos que recurrir al hook useContext desde cualquier componente descendiente, de forma que podamos leer el valor del contexto desde ahí.
Por empezar por un ejemplo sencillo, vamos a envolver el elemento estándar <button> en un componente Boton que va a añadirle automáticamente un estilo en función del tema que estemos utilizando:
const Boton = (props) => { const tema = useContext(ContextoTema) return ( <button style={{ background: tema.fondo2, color: tema.texto }} {...props}></button> ) }
Vamos a analizar paso a paso qué consigue este componente:
- Recoge el valor actual del tema en la constante
temamediante la llamadauseContext(ContextoTema). Puesto queContextoTemaes una constante global del archivo, podemos utilizarla aquí.
La diferencia entre utilizar una variable global simple y usar el contexto es que, con este último, React puede volver a renderizar todos los componentes que deban cambiar si el contexto se ve modificado.
- La interfaz de usuario generada consiste en un elemento
<button>con la propiedadstyleque recibe un objeto de JavaScript: aunque sintácticamente se parezca a propiedades CSS, debemos recordar que estamos escribiendo JavaScript y, por ejemplo, los nombres de propiedades con guiones se convierten a camelCase (backgroundColoren lugar debackground-color). - Proporcionamos el objeto de propiedades
propsdesestructurándolo de la forma{…props}, para que cualquier propiedad extra que asociemos al elemento<Boton>se aplique en su lugar al<button>.
El objeto recibido en props que se desestructura, incluye la propiedad especial children (que apuntamos en el módulo anterior y que veremos luego con más detalle), por lo que a través de ella podremos especificar un contenido a nuestro botón que se transferirá al elemento HTML correspondiente (en este caso un <button> estándar de HTML). Lo entenderás mejor más adelante, pero es interesante resaltarlo aquí por completitud.
Sustituyendo ahora los elementos <button> de nuestra aplicación por componentes <Boton> (únicamente debemos cambiar los nombres de las etiquetas) y realizando un proceso análogo con la propiedad style en el componente ListaTareas, obtendremos un resultado similar al siguiente:
Al pulsar el botón “Activar tema oscuro”, se alterarán los colores de los botones:
El componente <ContextoTema.Provider> se puede utilizar más de una vez, incluso con distintos valores, para modificar el contexto en ciertas partes de la aplicación si nos interesa.
Desarrollo de formularios con React
Una vez que conoces las herramientas para disponer de variables con estado y de gestores de eventos, vamos a ponerlas en práctica implementando un formulario de creación de una cuenta de usuario en nuestra aplicación web. Este formulario incluirá controles de entrada de datos de diversa índole, será capaz de validar los datos introducidos e incluso podrá mostrar u ocultar campos en función de dichos datos.
El componente FormularioSignup
El primer paso para construir el formulario de creación de nuevas cuentas es crear un nuevo componente. Lo vamos a denominar FormularioSignup y, para comenzar, contendrá un campo para el usuario y otro para la contraseña. En puro HTML, el formulario se compondría aproximadamente como sigue:
<form> <fieldset> <legend>Crear una cuenta</legend> <p>Con una cuenta de usuario podrás guardar tus tareas y consultarlas en cualquier dispositivo.</p> <label for="usuario">Nombre de usuario</label> <input type="text" name="usuario" id="usuario" placeholder="alice" /> <label for="pass">Contraseña</label> <input type="password" name="pass" id="pass" /> </fieldset> </form>
A la hora de convertir este código en un formulario para React, prestaremos especial atención a los atributos que cambien de nombre (como for a htmlFor) y transformaremos los campos de entrada en campos controlados. Para esto, introduciremos los datos con estado usuario y contrasena.
const FormularioSignup = () => { const [usuario, setUsuario] = useState("") const [contrasena, setContrasena] = useState("") return ( <form> <fieldset> <legend>Crear una cuenta</legend> <p>Con una cuenta de usuario podrás guardar tus tareas y consultarlas en cualquier dispositivo.</p> <label htmlFor="usuario">Nombre de usuario</label> <input type="text" name="usuario" id="usuario" placeholder="alice" value={usuario} onChange={e => setUsuario(e.target.value)} /> <label htmlFor="pass">Contraseña</label> <input type="password" name="pass" id="pass" value={contrasena} onChange={e => setContrasena(e.target.value)} /> <p><input type="submit" value="Crear cuenta" /></p> </fieldset> </form> ) }
Incluyendo algo de estilo para el formulario y estos campos, obtendríamos un resultado similar al de la siguiente imagen:
Vamos a ver cómo convertir este formulario HTML en un componente React, con validaciones, otros campos diferentes, campos condicionales y partes de la UI que se muestren condicionalmente, etc.
Validación de campos
Siguiendo con nuestro formulario, a continuación, vamos a establecer un mecanismo para verificar el contenido de los campos usuario y contraseña, y trabajaremos de forma análoga para el resto de campos que incluyamos en el formulario. El objetivo será que, si el valor del campo no cumple con la condición que impongamos sobre él, se muestre un mensaje aclaratorio indicando el requisito que no se está cumpliendo.
En nuestro componente FormularioSignup, vamos a organizar los indicadores de error en un objeto como sigue:
const errores = {
usuario: usuario.length < 3, contrasena: contrasena.length < 8
}
Puesto que React llama a la función de renderizado del componente cada vez que cambia el estado de usuario o contrasena, basta con que calculemos esos estados de error para el caso actual, ya que, en caso de que se introduzca un nuevo valor, se recalcularán al volver a renderizar.
En la interfaz del formulario, podemos ahora utilizar renderizado condicional para incluir un mensaje de error únicamente cuando el valor errores.usuario o errores.contrasena sea verdadero:
<label htmlFor=“usuario”>Nombre de usuario</label> <input type=“text” name=“usuario” id=“usuario” placeholder=“alice”
value={usuario} onChange={e => setUsuario(e.target.value)} />
{errores.usuario &&
<p className="error">El nombre de usuario debe tener al menos 3 caracteres.</p>}
<label htmlFor=“pass”>Contraseña</label> <input type=“password” name=“pass” id=“pass”
value={contrasena} onChange={e => setContrasena(e.target.value)} />
{errores.contrasena &&
<p className="error">La contraseña debe tener al menos 8 caracteres.</p>}
Para simplificar un poco este código y poder ampliar el número de campos sin repetir demasiado marcado, vamos a extraer el esqueleto de cada control individual a un nuevo componente Campo:
const Campo = ({ id, children, invalido = false, error = “”, onValueChange = () ⇒ {}, …props }) ⇒ {
return (
<div className="campo">
<label htmlFor={id}>{ children }</label>
<input {...props} name={id} id={id} onChange={e => onValueChange(e.target.value)} />
{invalido && <p className='error'>{error}</p>}
</div>
)
}
Como puedes observar, este componente nos ahorra el trabajo de definir los detalles menores del marcado HTML y de mostrar el mensaje de error en caso necesario. Además, estamos proporcionando la propiedad especial children como hija de la etiqueta <label> de forma que podamos especificar el nombre del campo en el texto del nodo. De forma similar, propiedades que dependerán del caso concreto como type o placeholder se dejan en el objeto props, con el resto de propiedades, y se pasan directamente al elemento <input>.
Ahora, sustituimos en el componente FormularioSignup las etiquetas previas por un componente Campo y la interfaz queda como en el siguiente listado:
<form>
<fieldset>
<legend>Crear una cuenta</legend>
<p>Con una cuenta de usuario podrás guardar tus tareas y consultarlas en cualquier dispositivo.</p>
<Campo id="usuario" type="text" placeholder="alice"
value={usuario} onValueChange={setUsuario} invalido={errores.usuario}
error="El nombre de usuario debe tener al menos 3 caracteres.">
Nombre de usuario
</Campo>
<Campo id="pass" type="password" value={contrasena} onValueChange={setContrasena}
invalido={errores.contrasena} error="La contraseña debe tener al menos 8 caracteres.">
Contraseña
</Campo>
<p><input type="submit" value="Crear cuenta" /></p> </fieldset>
</form>
Esto daría como resultado una pantalla como la siguiente:
Vista actual del formulario con dos mensajes de dato inválido
Puedes comprobar que, al introducir un nombre de al menos 3 caracteres y una contraseña de al menos 8, los mensajes correspondientes desaparecerán. Otros tipos de campos
React incluye soporte para muchos tipos de controles de formularios. Además, los uniformiza de forma que siempre podemos acceder al valor con la propiedad value, ya sea para leerlo o para fijarlo. Vamos a explorar diferentes tipos de campos en el formulario extendiéndolo con las siguientes opciones:
Un campo tipo email para el correo electrónico Un campo tipo date para la fecha de nacimiento Un campo textarea para introducir una biografía Una lista de selección para elegir entre un plan gratuito y uno de pago
Asociados a estos nuevos datos, necesitaremos variables en el estado del componente para controlarlos:
const [email, setEmail] = useState(“”) const [fechaNacimiento, setFechaNacimiento] = useState(“”) const [bio, setBio] = useState(“”) const [plan, setPlan] = useState(“g”)
Asimismo, incluiremos nuevas condiciones para validar el contenido del email y la fecha de nacimiento, que no debe ser posterior al día actual:
const errores = {
usuario: usuario.length < 3,
email: email.match(/^[^@]+@[a-z0-9\-\.]+\.[a-z]{2,}$/i) === null,
fechaNacimiento: Date.parse(fechaNacimiento) >= Date.now(),
contrasena: contrasena.length < 8
}
Por último, haremos uso del componente Campo para los controles basados en el elemento input (pasándole el tipo adecuado en el atributo/propiedad type estándar en este control), y añadiremos al final el control <textarea> y el <select>:
<form>
<fieldset>
<legend>Crear una cuenta</legend>
<p>Con una cuenta de usuario podrás guardar tus tareas y consultarlas en cualquier dispositivo.</p>
<Campo id="usuario" type="text" placeholder="alice"
value={usuario} onValueChange={setUsuario} invalido={errores.usuario}
error="El nombre de usuario debe tener al menos 3 caracteres.">
Nombre de usuario
</Campo>
<Campo id="email" type="email" placeholder="alice@example.org" value={email} onValueChange={setEmail}
invalido={errores.email} error="Escribe una dirección de correo electrónico válida.">
Correo electrónico
</Campo>
<Campo id="pass" type="password" value={contrasena} onValueChange={setContrasena}
invalido={errores.contrasena} error="La contraseña debe tener al menos 8 caracteres.">
Contraseña
</Campo>
<Campo id="fechaNacimiento" type="date" value={fechaNacimiento} onValueChange={setFechaNacimiento}
invalido={errores.fechaNacimiento} error="La fecha de nacimiento no puede ser posterior al día actual.">
Fecha de nacimiento
</Campo>
<label htmlFor="bio">Biografía</label>
<textarea id='bio' name='bio' onChange={e => setBio(e.target.value)} value={bio} />
<label htmlFor="plan">Selecciona un plan</label>
<select value={plan} onChange={e => setPlan(e.target.value)} id="plan">
<option value="g">Gratuito (0€)</option>
<option value="p">Pro (12€/año)</option>
</select>
<p><input type="submit" value="Crear cuenta" /></p> </fieldset>
</form>
Fíjate en cómo difieren <textarea> y <select> de los elementos correspondientes en HTML: en este caso, el contenido del <textarea> se asigna con la propiedad value en lugar de como nodo de texto hijo. De forma similar, la lista de selección utiliza la misma propiedad en lugar de un atributo selected en uno de los nodos hijo. Esto facilita mucho el trabajo a la hora de recorrer los valores de los controles del formulario.
Como resultado, obtenemos un formulario similar al de la siguiente imagen:
Aspecto actual del formulario sin ninguna entrada
