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.
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:
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?
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)
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.
Utilizar una librería como Yup en lugar de implementar validaciones manuales ofrece varios beneficios:
Para instalar Yup, podemos usar npm o yarn.
Npm
npm install yup
Yarn
yarn add yup
Repositorio oficial de la librería 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:
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:
Ahora veamos como implementar el mismo ejercicio usando Yup y las librerías más populares para crear formularios.
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
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
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
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