Cómo hacer un formulario con React, desde 0

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.


¿Qué es un formulario?

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.


Para entenderlo mejor, veamos la figura 1.

Figura 1. Ejemplo de un formulario Los campos correo, cursos y suscripción activa en este formulario nos ayudan a mantener la data estructurada.


¿Cuando usar un formulario?

Los formularios los utilizamos principalmente cuando necesitamos que el usuario utilice algún “feature” de nuestra aplicación.


Por ejemplo:


  • Registro o inicio de sesión
  • Búsquedas
  • Encuestas o cuestionarios
  • Compras en línea
  • Comentarios o reseñas
  • Configuraciones personas
  • Contacto con soporte

¿Características principales de un formulario?

Algunas de las características principales que podríamos encontrar son:


  • Campos. Los campos más comunes son de tipo texto, numérico, de fecha, de selección.
  • Acción para guardar campos. Sirven para enviar los campos al backend. Los encontramos como botones y normalmente, se encuentran en la parte final del formulario.
  • Acción para cancelar. Se ocupan para revertir algún cambio en el formulario. Al igual, los podemos encontrar como botones y se ubican a un costado de la acción para guardar.
  • Validaciones. Las validaciones podemos encontrar de dos tipos: validaciones del frontend y validaciones de backend. Ambas, validan que la data ingresada por el usuario tenga el formato correcto o que cumpla con alguna regla de negocio.
  • Notificaciones. Comunican si existe algún error o si todo ha salido ok. 


¿Cómo hacer un formulario básico en React?

Para este ejemplo, supongamos que somos una veterinaria que necesita un formulario para agendar citas. Los requerimientos son los siguientes:


  • Nombre del cliente
  • Nombre de la mascota
  • Raza de la mascota
  • Sobre su mascota
  • Fecha de cita

Ya teniendo contexto sobre los requerimientos, es hora de poner toda nuestra creatividad para solucionar este problema.

Diseño

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.


Lo primero, que haremos es definir el flujo principal de comunicación del formulario. Ver figura 2.

Figura 2. Flujo de un formulario básico. El usuario ingresa los datos, se validan los formados de cada campo, se envían los datos al backend, el backend devuelve una respuesta y finalmente, el frontend muestra la respuesta al usuario.


El flujo es bastante sencillo:
  1. Usuario ingresa los datos
  1. Se validan los formados de cada campo
  1. Envía data al backend
  1. Backend devuelve una respuesta
  1. Frontend muestra la respuesta al usuario

Ya que sabemos el flujo principal, saltemos al diseño de nuestros componentes en React. Ver figura 3.

Figura 3. Componentes para un formulario básico en React. El formulario se compone a partir de cinco componentes: Form, TextField, TextAreaField, SelectField y ActionField, cada uno encargado de una tarea específica.


Para construir nuestro formulario ocupamos de cinco componentes:
  • Form. Encargado de renderizar los componentes TextField, TextAreaField, SelectField y ActionsField y mantener la comunicación entre cada uno.
  • TextField. Permite ingresar campos de tipo texto.
  • TextAreaField. Se utiliza para capturar texto más extenso.
  • SelectField. Muestra una lista de opciones.
  • ActionField. Representa las acciones del formulario, como guardar o cancelar.

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: }
. . .




Componente ActionField

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:


  • Mantener un estado global de todos los campos.
  • Permitir solo componentes de tipo Text, TextArea, Select y Action.
  • Renderizar los componentes en una ubicación específica.
  • Prop onSubmit de tipo callback, que se ejecuta cuando se hace clic en la acción submit.
  • Prop onCancel de tipo callback, que se ejecuta cuando se hace clic en la acción cancel.

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.


Resultado final


    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;

Alternativas y librerías recomendadas

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.

Formik

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í.


FinalForm

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.

ReactHookForm

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…


Material UI

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í.


Conclusión

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.