Validaciones en formularios de React usando Yup

Gabriel Jiménez | Hace alrededor de 1 mes

En este artículo aprenderás a utilizar Yup para validar tus formularios, ya sea usando una librería externa o algo más casero, cualquiera de las dos opciones, Yup se adapta perfectamente.


¿Por qué validar formularios en React es importante?

Los formularios son elementos esenciales en el desarrollo frontend, porque permiten que el usuario interactúe con los servicios del negocio. Si no facilitamos una comunicación efectiva, seguro perderemos un posible cliente. Una de las estrategias es usar validaciones en nuestros formularios. 


Algunas ventajas de implementarlas:


  • Datos consistentes y confiables. Reducen errores de formato, como correos mal escritos o números inválidos.
  • Mejorar la experiencia del usuario. Los usuarios reciben retroalimentación inmediata cuando algo falta o está mal escrito.
  • Menos validaciones en el backend. Al filtrar datos incorrectos desde el frontend, minimizamos el procesamiento innecesario en el servidor.
  • Procesos más eficientes. Al tener información correcta desde el inicio, evitamos reprocesamiento y validaciones extras.
  • Mayor seguridad. Al controlar lo que entra, disminúyenos el riesgo de datos maliciosos o inconsistentes.

¿Qué es Yup y por qué usarlo en React?

Yup es una librería de Javascript para transformar y validar datos. Mmmm, muy bonita la definición pero en términos mortales. ¿Qué significa?


Validación de campos

Supongamos que queremos validar si un objeto devuelto por el backend tiene la estructura correcta. 


Sin utilizar Yup:

const user = await fetchUser()

// Campo obligatorio y con formato de correo
if(user.email && validarFormatoCorreo(user.email)) {}


Ahora bien, para lograr el mismo resultando usando Yup. 

Hay que hacer uso del generador de esquemas. Un esquema lo podemos ver como un molde, donde definimos una estructura especifica que queremos evaluar.


Utilizando Yup:

const user = await fetchUser()

// Campo obligatorio y con formato de correo
let userSchema = object({
  email: string().email().required(),
});

await userSchema.validate(user)

Transformación de datos

Ya hemos validado el formato del correo, pero ahora queremos mostrar todos los correos con un mismo formato: todos los correos en minúsculas.


Sin utilizar Yup:

const user = await fetchUser()

// Campo obligatorio y con formato de correo
if(user.email && validarFormatoCorreo(user.email)) {
  user.email = user.email.toLowerCase()
}


Yup simplifica esa conversión al usar funciones transformadoras.


Utilizando Yup:

let userSchema = object({
  email: string().email()
   .lowercase() // Función transformadora
   .required(),
});

const user = await userSchema.validate(user)


Al imprimir el correo del usuario, este estará en minúsculas.


Ventajas frente a validaciones manuales

Utilizar una librería como Yup en lugar de implementar validaciones manuales ofrece varios beneficios:


  • Menos código repetitivo. Al tener funciones ya desarrolladas, evitamos volver a escribir la misma lógica dos veces.
  • Menos propenso a errores. Al ser una librería bien probadas y con definiciones claras, reducimos los errores.
  • Mayor flexibilidad. Permite validar casos simples hasta complejos.
  • Buena documentación. Cuenta con ejemplos claros.
  • Amplia comunidad. Al ser de las librerías más populares para validar, es fácil encontrar soluciones para cada caso de uso.
  • Compatible con múltiples librerías o frameworks. Al ser una librería javascript, puede usarse en cualquier librería o framework como Angular, ReactJS, React Native, VueJS, etc.

Instalación y configuración de Yup en React

Para instalar Yup, podemos usar npm o yarn.

Npm

npm install yup


Yarn

yarn add yup


Repositorio oficial de la librería Yup


Ejemplo práctico de validación con Yup

Para entender mejor las ventajas de Yup frente a las validaciones manuales, empezaremos con un ejercicio práctico sin utilizar ninguna librería. Después, implementaremos Yup para simplificar la lógica y finalmente, veremos cómo integrarlo con librerías especializadas en formularios como React Hook Form, React Final Form y Formik.


Nuestro caso de uso es el siguiente: 


Necesitamos un formulario con tres campos: nombre, fecha y observaciones. Donde todos los campos son obligatorios, excepto el campo observaciones. Además, el campo fecha solo permite seleccionar días entre semana.


Implementación sin Yup

    1: import React, { useState } from 'react'
    2: 
    3: function CitasForm() {
    4:   const [values, setValues] = useState({})
    5:   const [errors, setErrors] = useState({})
    6: 
    7:   function handleChange({ target }) {
    8:     setValues({ ...values, [target.name]: target.value })
    9:   }
   10: 
   11:   function isWeekend(dateString) {
   12:     const date = new Date(dateString)
   13:     const day = date.getDay()
   14:     return day === 0 || day === 6
   15:   }
   16: 
   17:   function handleSubmit(e) {
   18:     e.preventDefault()
   19:     const newErrors = {}
   20: 
   21:     if (!values.nombre) {
   22:       newErrors.nombre = "El nombre es obligatorio"
   23:     }
   24: 
   25:     if (!values.fecha) {
   26:       newErrors.fecha = "La fecha es obligatoria"
   27:     } else if (isWeekend(values.fecha)) {
   28:       newErrors.fecha = "La fecha no puede ser en fin de semana"
   29:     }
   30: 
   31:     setErrors(newErrors)
   32: 
   33:     if (Object.keys(newErrors).length === 0) {
   34:       console.log("Datos válidos:", values)
   35:     }
   36:   }
   37: 
   38:   return (
   39:     <form onSubmit={handleSubmit}>
   40:       <input
   41:         type="text"
   42:         name="nombre"
   43:         placeholder="Nombre"
   44:         onChange={handleChange}
   45:       />
   46:       {errors.nombre && <span>{errors.nombre}</span>}
   47: 
   48:       <input
   49:         type="date"
   50:         name="fecha"
   51:         placeholder="Fecha"
   52:         onChange={handleChange}
   53:       />
   54:       {errors.fecha && <span>{errors.fecha}</span>}
   55: 
   56:       <textarea
   57:         name="observaciones"
   58:         placeholder="Observaciones"
   59:         onChange={handleChange}
   60:       />
   61: 
   62:       <button type="submit">Enviar</button>
   63:     </form>
   64:   )
   65: }
   66: 
   67: export default CitasForm


La implementación es bastante sencilla, ¿cierto?

Sin embargo, hay varias cosas que tener cuenta al implementar las validaciones de esta manera:


  • Reutilización de reglas. Las validaciones de obligatoriedad se repiten con frecuencia, por lo que resulta más práctico encapsularlas en una función genérica y reutilizable.
  • Expresividad del código. Usar condiciones como !values.fecha no comunica claramente la intención. Una función con nombre explícito, por ejemplo isRequired(), hace que el propósito sea más entendible.
  • Momento de mostrar errores. Actualmente los mensajes solo aparecen al enviar el formulario. ¿Qué ocurre si queremos mostrarlos cuando el usuario termina de escribir en un campo (por ejemplo, en el evento onBlur)? Tendríamos que crear una función para encapsular la lógica de validación del handleSubmit.
  • Ocultar mensajes de error. Si un error ocurre y se corrige, el mensaje de error desaparece hasta que se vuelve a enviar el formulario.

Además de estos errores, existen otros que tienen que ver con el flujo del formulario. Si quieres saber más sobre eso, te invito a leer alguno de mis artículos:


Formik en React: cómo crear tu primer formulario desde 0

Tu primer formulario en React con React Hook Form

Formularios con React: tutorial paso a paso con Hook Form y MUI

Ahora veamos como implementando Yup, puede mejorar mucho nuestro código.

    1: import React, { useState } from 'react'
    2: import * as yup from 'yup'
    3: 
    4: const schema = yup.object({
    5:   nombre: yup.string().required('El nombre es obligatorio'),
    6:   fecha: yup
    7:     .string()
    8:     .required('La fecha es obligatoria')
    9:     .test('not-weekend', 'La fecha no puede ser en fin de semana', (val) => {
   10:       if (!val) return false
   11:      
   12:       const d = new Date(`${val}T00:00:00Z`)
   13:       const day = d.getUTCDay()
   14:       return day !== 0 && day !== 6
   15:     }),
   16:   observaciones: yup.string().optional(),
   17: })
   18: 
   19: function CitasForm() {
   20:   const [values, setValues] = useState({ nombre: '', fecha: '', observaciones: '' })
   21:   const [errors, setErrors] = useState({})
   22: 
   23:   function handleChange({ target }) {
   24:     setValues({ ...values, [target.name]: target.value })
   25:   }
   26: 
   27:   async function handleBlur({ target }) {
   28:     try {
   29:       await schema.validateAt(target.name, values)
   30:       setErrors((prev) => ({ ...prev, [target.name]: undefined }))
   31:     } catch (err) {
   32:       setErrors((prev) => ({ ...prev, [target.name]: err.message }))
   33:     }
   34:   }
   35: 
   36:   async function handleSubmit(e) {
   37:     e.preventDefault()
   38:     try {
   39:       await schema.validate(values, { abortEarly: false })
   40:       setErrors({})
   41:       console.log('Datos válidos:', values)
   42:     } catch (err) {
   43:       if (err.inner) {
   44:         const next = {}
   45:         err.inner.forEach((e) => {
   46:           if (!next[e.path]) next[e.path] = e.message
   47:         })
   48:         setErrors(next)
   49:       }
   50:     }
   51:   }
   52: 
   53:   return (
   54:     <form onSubmit={handleSubmit}>
   55:       <input
   56:         type="text"
   57:         name="nombre"
   58:         placeholder="Nombre"
   59:         value={values.nombre}
   60:         onChange={handleChange}
   61:         onBlur={handleBlur}
   62:       />
   63:       {errors.nombre && <span>{errors.nombre}</span>}
   64: 
   65:       <input
   66:         type="date"
   67:         name="fecha"
   68:         placeholder="Fecha"
   69:         value={values.fecha}
   70:         onChange={handleChange}
   71:         onBlur={handleBlur}
   72:       />
   73:       {errors.fecha && <span>{errors.fecha}</span>}
   74: 
   75:       <textarea
   76:         name="observaciones"
   77:         placeholder="Observaciones"
   78:         value={values.observaciones}
   79:         onChange={handleChange}
   80:         onBlur={handleBlur}
   81:       />
   82:       {errors.observaciones && <span>{errors.observaciones}</span>}
   83: 
   84:       <button type="submit">Enviar</button>
   85:     </form>
   86:   )
   87: }
   88: 
   89: export default CitasForm


Al usar Yup, hemos mejorado lo siguiente:


  • Centralizamos la lógica para definir las validaciones, usando esquemas.
  • Reutilización de funciones como require, string.
  • Funciones más expresivas.
  • Implementación más sencilla para validar cuando el usuario termina de escribir.

Ahora veamos como implementar el mismo ejercicio usando Yup y las librerías más populares para crear formularios.


Yup con React Hook Form


    1: import React, { useState } from 'react'
    2: import * as yup from 'yup'
    3: 
    4: const schema = yup.object({
    5:   nombre: yup.string().trim().required('El nombre es obligatorio'),
    6:   fecha: yup
    7:     .string()
    8:     .required('La fecha es obligatoria')
    9:     .test('not-weekend', 'La fecha no puede ser en fin de semana', (val) => {
   10:       if (!val) return false
   11:      
   12:       const d = new Date(`${val}T00:00:00Z`)
   13:       const day = d.getUTCDay()
   14:       return day !== 0 && day !== 6
   15:     }),
   16:   observaciones: yup.string().optional(),
   17: })
   18: 
   19: function CitasForm() {
   20:   const [values, setValues] = useState({ nombre: '', fecha: '', observaciones: '' })
   21:   const [errors, setErrors] = useState({})
   22: 
   23:   function handleChange({ target }) {
   24:     setValues({ ...values, [target.name]: target.value })
   25:   }
   26: 
   27:   async function handleBlur({ target }) {
   28:     try {
   29:       await schema.validateAt(target.name, values)
   30:       setErrors((prev) => ({ ...prev, [target.name]: undefined }))
   31:     } catch (err) {
   32:       setErrors((prev) => ({ ...prev, [target.name]: err.message }))
   33:     }
   34:   }
   35: 
   36:   async function handleSubmit(e) {
   37:     e.preventDefault()
   38:     try {
   39:       await schema.validate(values, { abortEarly: false })
   40:       setErrors({})
   41:       console.log('Datos válidos:', values)
   42:     } catch (err) {
   43:       if (err.inner) {
   44:         const next = {}
   45:         err.inner.forEach((e) => {
   46:           if (!next[e.path]) next[e.path] = e.message
   47:         })
   48:         setErrors(next)
   49:       }
   50:     }
   51:   }
   52: 
   53:   return (
   54:     <form onSubmit={handleSubmit}>
   55:       <input
   56:         type="text"
   57:         name="nombre"
   58:         placeholder="Nombre"
   59:         value={values.nombre}
   60:         onChange={handleChange}
   61:         onBlur={handleBlur}
   62:       />
   63:       {errors.nombre && <span>{errors.nombre}</span>}
   64: 
   65:       <input
   66:         type="date"
   67:         name="fecha"
   68:         placeholder="Fecha"
   69:         value={values.fecha}
   70:         onChange={handleChange}
   71:         onBlur={handleBlur}
   72:       />
   73:       {errors.fecha && <span>{errors.fecha}</span>}
   74: 
   75:       <textarea
   76:         name="observaciones"
   77:         placeholder="Observaciones"
   78:         value={values.observaciones}
   79:         onChange={handleChange}
   80:         onBlur={handleBlur}
   81:       />
   82:       {errors.observaciones && <span>{errors.observaciones}</span>}
   83: 
   84:       <button type="submit">Enviar</button>
   85:     </form>
   86:   )
   87: }
   88: 
   89: export default CitasForm

Yup con React Final Form


    1: import React from 'react'
    2: import { Form, Field } from 'react-final-form'
    3: import * as yup from 'yup'
    4: 
    5: const schema = yup.object({
    6:   nombre: yup.string().trim().required('El nombre es obligatorio'),
    7:   fecha: yup
    8:     .string()
    9:     .required('La fecha es obligatoria')
   10:     .test('not-weekend', 'La fecha no puede ser en fin de semana', (val) => {
   11:       if (!val) return false
   12:      
   13:       const d = new Date(`${val}T00:00:00Z`)
   14:       const day = d.getUTCDay()
   15:       return day !== 0 && day !== 6
   16:     }),
   17:   observaciones: yup.string().optional(),
   18: })
   19: 
   20: // Validador a nivel de formulario usando Yup (sin resolvers externos)
   21: async function validate(values) {
   22:   try {
   23:     await schema.validate(values, { abortEarly: false })
   24:     return {}
   25:   } catch (err) {
   26:     const formErrors = {}
   27:     if (err.inner) {
   28:       err.inner.forEach((e) => {
   29:         if (e.path && !formErrors[e.path]) formErrors[e.path] = e.message
   30:       })
   31:     } else if (err.path) {
   32:       formErrors[err.path] = err.message
   33:     }
   34:     return formErrors
   35:   }
   36: }
   37: 
   38: function CitasForm() {
   39:   async function onSubmit(data) {
   40:     console.log('Datos válidos:', data)
   41:   }
   42: 
   43:   return (
   44:     <Form
   45:       onSubmit={onSubmit}
   46:       validate={validate}
   47:       validateOnBlur
   48:       initialValues={{ nombre: '', fecha: '', observaciones: '' }}
   49:       render={({ handleSubmit }) => (
   50:         <form onSubmit={handleSubmit}>
   51:           <Field name="nombre">
   52:             {({ input, meta }) => (
   53:               <>
   54:                 <input {...input} type="text" placeholder="Nombre" />
   55:                 {meta.touched && meta.error && <span>{meta.error}</span>}
   56:               </>
   57:             )}
   58:           </Field>
   59: 
   60:           <Field name="fecha">
   61:             {({ input, meta }) => (
   62:               <>
   63:                 <input {...input} type="date" placeholder="Fecha" />
   64:                 {meta.touched && meta.error && <span>{meta.error}</span>}
   65:               </>
   66:             )}
   67:           </Field>
   68: 
   69:           <Field name="observaciones">
   70:             {({ input, meta }) => (
   71:               <>
   72:                 <textarea {...input} placeholder="Observaciones" />
   73:                 {meta.touched && meta.error && <span>{meta.error}</span>}
   74:               </>
   75:             )}
   76:           </Field>
   77: 
   78:           <button type="submit">Enviar</button>
   79:         </form>
   80:       )}
   81:     />
   82:   )
   83: }
   84: 
   85: export default CitasForm

Yup con Formik


    1: import React from 'react'
    2: import { Formik } from 'formik'
    3: import * as yup from 'yup'
    4: 
    5: const schema = yup.object({
    6:   nombre: yup.string().trim().required('El nombre es obligatorio'),
    7:   fecha: yup
    8:     .string()
    9:     .required('La fecha es obligatoria')
   10:     .test('not-weekend', 'La fecha no puede ser en fin de semana', (val) => {
   11:       if (!val) return false
   12:      
   13:       const d = new Date(`${val}T00:00:00Z`)
   14:       const day = d.getUTCDay()
   15:       return day !== 0 && day !== 6
   16:     }),
   17:   observaciones: yup.string().optional(),
   18: })
   19: 
   20: function CitasForm() {
   21:   return (
   22:     <Formik
   23:       initialValues={{ nombre: '', fecha: '', observaciones: '' }}
   24:       validationSchema={schema}
   25:       validateOnBlur
   26:       validateOnChange={false} // solo valida al blur y al submit
   27:       onSubmit={(values) => {
   28:         console.log('Datos válidos:', values)
   29:       }}
   30:     >
   31:       {({ values, errors, touched, handleChange, handleBlur, handleSubmit }) => (
   32:         <form onSubmit={handleSubmit}>
   33:           <input
   34:             type="text"
   35:             name="nombre"
   36:             placeholder="Nombre"
   37:             value={values.nombre}
   38:             onChange={handleChange}
   39:             onBlur={handleBlur}
   40:           />
   41:           {touched.nombre && errors.nombre && <span>{errors.nombre}</span>}
   42: 
   43:           <input
   44:             type="date"
   45:             name="fecha"
   46:             placeholder="Fecha"
   47:             value={values.fecha}
   48:             onChange={handleChange}
   49:             onBlur={handleBlur}
   50:           />
   51:           {touched.fecha && errors.fecha && <span>{errors.fecha}</span>}
   52: 
   53:           <textarea
   54:             name="observaciones"
   55:             placeholder="Observaciones"
   56:             value={values.observaciones}
   57:             onChange={handleChange}
   58:             onBlur={handleBlur}
   59:           />
   60:           {touched.observaciones && errors.observaciones && <span>{errors.observaciones}</span>}
   61: 
   62:           <button type="submit">Enviar</button>
   63:         </form>
   64:       )}
   65:     </Formik>
   66:   )
   67: }
   68: 
   69: export default CitasForm

Conclusión

Las validaciones son de gran ayuda para permitir una comunicación efectiva entre el usuario final y los servicios de nuestras aplicaciones. Herramientas como Yup facilitan y centralizar la lógica de validación. Por último, es muy sencillo implementar Yup usando librerías especializadas para la creación de formularios.


Si quieres aprender más sobre como utilizar pruebas unitarias y de integración en tus componentes React, te invito a leer mi libro: Testing en React: Guía práctica con Jest y React Testing Library