
Gabriel Jiménez | Hace 7 días
Muchos de los frontend como web, móvil y de escritorio están llenos de formularios. Estos elementos nos permiten que el usuario interactúa con el backend de forma transparente. Para entenderlos mejor, comencemos definiendo qué es un formulario, cuando usarlos y las características principales. Al final, haremos un formulario básico en React.
Un formulario es un elemento que permite al usuario poder ingresar datos de manera estructurada mediante campos. Los campos se utilizan para especificar el tipo de dato que esperamos. De esta forma, guiamos al usuario sobre la información que debe de ingresar.
Los formularios los utilizamos principalmente cuando necesitamos que el usuario utilice algún “feature” de nuestra aplicación.
Por ejemplo:
¿Características principales de un formulario?
Algunas de las características principales que podríamos encontrar son:
Para este ejemplo, supongamos que somos una veterinaria que necesita un formulario para agendar citas. Los requerimientos son los siguientes:
Ya teniendo contexto sobre los requerimientos, es hora de poner toda nuestra creatividad para solucionar este problema.
El diseño es una etapa que todos deberíamos tener en cuenta, porque sin un buen diseño, difícilmente nuestras aplicaciones van a escalar y ser mantenibles.
Listo.
NOTA: El diseño es importante porque nos guía y nos permite tener una visión más global sobre lo que estamos construyendo. ¡No lo subestimes!
Implementación
Comencemos definiendo los esqueletos de nuestros componentes, para luego atacarlos uno por uno.
src/components/Form/Form.js 1: import React from 'react'; 2: 3: const Form = () => { 4: return null 5: } 6: 7: export const TextField = () => { 8: return null 9: } 10: 11: export const TextAreaField = () => { 12: return null 13: } 14: 15: export const SelectField = () => { 16: return null 17: } 18: 19: export const ActionField = () => { 20: return null 21: } 22: 23: export default Form
Componente TextField
La responsabilidad de este componente es que permita escribir texto al usuario. Para lograrlo, creemos un estado interno para sincronizar lo que el usuario escriba. Además, recibe dos props: identificador y name.
src/components/Form/Form.js . . . 7: export const TextField = ({ name, id, children, type, value, onChange }) => { 8: const label = typeof children === "string" ? children : "" 9: 10: return( 11: <div className='textfield'> 12: <label htmlFor={id}>{label}</label> 13: <input type={type} name={name} id={id} value={value} onChange={onChange} /> 14: </div> 15: ) 16: } . . .
Para utilizarlo:
<TextField id=“nombre” name=“nombre">Mi Campo</TextField>
NOTA: Nombrar correctamente los props es tan importante porque pueden cooperar a que tus componentes sean más escalables y mantenibles. Si quieres aprender más, revisa mi artículo: React props: buenas prácticas para código más limpio y escalable
Componente TextAreaField
Es muy similar al TextField: mantenemos un estado interno para sincronizar lo que el usuario escribe y recibimos un identificador y un name como props.
src/components/Form/Form.js . . . 22: export const TextAreaField = ({ name, id, children, value, onChange }) => { 23: const label = typeof children === "string" ? children : "" 24: 25: return( 26: <div className='textareafield'> 27: <label htmlFor={id}>{label}</label> 28: <textarea name={name} id={id} value={value} onChange={onChange} /> 29: </div> 30: ) 31: } . . .
Se utiliza igual que TextField:
<TextAreaField id=“nombre” name=“nombre">Mi Campo</TextAreaField>
Componente SelectField
Muy similar a los anteriores. Sin embargo, recibe un prop options para listar las opciones que el usuario puede elegir.
src/components/Form/Form.js . . . 37: export const SelectField = ({ name, id, options, children, value, onChange }) => { 38: const label = typeof children === "string" ? children : "" 39: 40: return( 41: <div className='selectfield'> 42: <label htmlFor={id}>{label}</label> 43: <select name={name} id={name} value={value} onChange={onChange}> 44: <option>Selecciona una opción</option> 45: {options.map((option, index) => ( 46: <option key={index} value={option.value}>{option.label}</option> 47: ))} 48: </select> 49: </div> 50: ) 51: } . . .
Debe permitir elegir el tipo de acción: acción para guardar o cancelar.
src/components/Form/Form.js . . . 63: export const ActionField = ({ type , children, onCancel }) => { 64: const label = typeof children === "string" ? children : "" 65: 66: switch (type) { 67: case 'submit': 68: return( 69: <button type='submit'> 70: {label} 71: </button> 72: ) 73: case 'cancel': 74: return ( 75: <button type='button' onClick={() => { 76: onCancel && onCancel() 77: 78: }}> 79: {label} 80: </button> 81: ) 82: default: 83: console.error('Unknown type : "' + type) 84: } 85: } . . .
Ya que tenemos todos los sub componentes, hay que integrarlos al componente form.
Componente Form
La idea general de este componente es:
src/components/Form/Form.js . . . 3: export const Form = ({ children, onSubmit, onCancel }) => { 4: const initialValues = {} 5: const fields = [] 6: const actions = [] 7: 8: Children.forEach(children, (child) => { 9: if (!child) return 10: if (child.type === ActionField) { 11: actions.push(child) 12: } else if ( 13: [TextField, TextAreaField, SelectField].includes(child.type) && 14: child.props?.name 15: ){ 16: initialValues[child.props.name] = "" 17: fields.push(child) 18: } 19: }) 20: 21: const [values, setValues] = useState(initialValues) 22: 23: function handleChange({ target }) { 24: const { name, value } = target 25: setValues((prev) => ({ ...prev, [name]: value })) 26: } 27: 28: function handleSubmit(e) { 29: e.preventDefault() 30: onSubmit?.(values) 31: } 32: 33: return ( 34: <form onSubmit={handleSubmit}> 35: {fields.map((child) => 36: cloneElement(child, { 37: value: values[child.props.name], 38: onChange: handleChange 39: }) 40: )} 41: 42: {actions.map((child) => 43: cloneElement(child, { 44: onCancel, 45: }) 46: )} 47: </form> 48: ) 49: } . . .
Listo.
Ahora solo falta verlo en acción.
1: import React from 'react' 2: import {ActionField, Form, SelectField, TextAreaField, TextField} from "./components/Form/Form"; 3: 4: import { useState } from "react" 5: 6: function App() { 7: const [errors, setErrors] = useState([]) 8: 9: function validate(values) { 10: const newErrors = [] 11: const today = new Date().toISOString().split("T")[0] // yyyy-mm-dd 12: 13: if (!values.nombre) newErrors.push("El nombre del cliente es obligatorio") 14: if (!values.mascota) newErrors.push("El nombre de la mascota es obligatorio") 15: if (!values.raza) newErrors.push("La raza es obligatoria") 16: if (!values.cita) { 17: newErrors.push("La fecha de cita es obligatoria") 18: } else if (values.cita < today) { 19: newErrors.push("La fecha debe ser hoy o posterior") 20: } 21: 22: return newErrors 23: } 24: 25: function handleSubmit(values) { 26: const validationErrors = validate(values) 27: if (validationErrors.length > 0) { 28: setErrors(validationErrors) 29: return 30: } 31: setErrors([]) 32: console.log("Formulario válido ✅", values) 33: } 34: 35: return ( 36: <Form 37: onSubmit={handleSubmit} 38: onCancel={() => console.log("Cancelado")} 39: > 40: <TextField name="nombre" id="nombre">Nombre del cliente</TextField> 41: <TextField name="mascota" id="mascota">Nombre de la mascota</TextField> 42: <SelectField 43: name="raza" 44: id="raza" 45: options={[ 46: { value: "chihuahua", label: "Chihuahua" }, 47: { value: "pastor-aleman", label: "Pastor Alemán" }, 48: ]} 49: > 50: Raza del perro 51: </SelectField> 52: <TextAreaField name="descripcion" id="descripcion">Sobre su mascota</TextAreaField> 53: <TextField name="cita" id="cita" type="date">Fecha de cita</TextField> 54: 55: <ActionField type="submit">Guardar</ActionField> 56: <ActionField type="cancel">Cancelar</ActionField> 57: 58: {errors.length > 0 && ( 59: <div className="form-errors"> 60: <h4>Errores:</h4> 61: <ul> 62: {errors.map((err, i) => ( 63: <li key={i}>{err}</li> 64: ))} 65: </ul> 66: </div> 67: )} 68: </Form> 69: ) 70: } 71: 72: export default App;
Como desarrolladores una de las practicas más comunes es no “reinventar la rueda”. A que me refiero con esto, si ya existen soluciones en el mercado es mejor utilizarlas, ya que en muchos casos se mantienen actualizados con los estándares actuales.
Una herramienta centrada específicamente en abstraer el estado de los formularios y facilitando las validaciones de cada campo.
import { Formik, Form, Field } from "formik" export default function MyForm() { return ( <Formik initialValues={{ name: "" }} onSubmit={(values) => console.log(values)} > {() => ( <Form> <label htmlFor="name">Nombre</label> <Field id="name" name="name" placeholder="Escribe tu nombre" /> <button type="submit">Enviar</button> </Form> )} </Formik> ) }
Si quieres saber más sobre Formik, haz clic aquí.
Una alternativa más ligera que Formik. Igualmente, buen manejo del ciclo de vida de los formularios.
import { Form, Field } from "react-final-form" export default function MyForm() { return ( <Form onSubmit={(values) => console.log(values)} render={({ handleSubmit }) => ( <form onSubmit={handleSubmit}> <label>Nombre</label> <Field name="name" component="input" placeholder="Escribe tu nombre" /> <button type="submit">Enviar</button> </form> )} /> ) }
Haz clic, para conocer más sobre FinalForm.
Crea formularios fácilmente. Se diferencia de los otros por su rendimiento, ya que evita renders innecesarios.
import { useForm } from "react-hook-form" export default function MyForm() { const { register, handleSubmit } = useForm() const onSubmit = (data) => console.log(data) return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="name">Nombre</label> <input id="name" {...register("name")} placeholder="Escribe tu nombre" /> <button type="submit">Enviar</button> </form> ) }
Visita la página de ReactHookForm para saber más…
No maneja el cliclo de vida de los formularios, se enfoca en facilitar los componentes ya diseñados para la construcción de formularios.
import { useState } from "react" import { TextField, Button } from "@mui/material" export default function MyForm() { const [name, setName] = useState("") const handleSubmit = (e) => { e.preventDefault() console.log({ name }) } return ( <form onSubmit={handleSubmit}> <TextField label="Nombre" value={name} onChange={(e) => setName(e.target.value)} /> <Button type="submit" variant="contained"> Enviar </Button> </form> ) }
Te interesa saber más sobre Material UI, haz clic aquí.
Los formularios son de los elementos más importantes que existen para comunicar al usuario con nuestros servicios. Diseñarlos correctamente desde un inicio, nos permitirá mantenerlos y escalarlos fácilmente.
Si quieres aprender más sobre cómo desarrollar formularios más complejos, te envío a leer mi libro: Testing en React: Guía práctica con Jest y React Testing Library.