Cuando desarrollamos aplicacioens Frontend usando React, una de las funcionalidades mas comunes es la validacion de formularios, la cual dependiendo de que tan grande o que tantos campos requieran nuestros formularios esta puede ser una tarea laboriosa.
Asi que para evitar rehacer las mismas validaciones para cada proyecto o formulario de tu aplicacion tenemos bibliotecas como React Hook Form la cual no solo funciona con React sino tambien otras bibliotecas como Nextjs o React Native
Introducción
React Hook Form es una poderosa biblioteca para trabajar con formularios en React, que busca simplificar la experiencia de creación de formularios proporcionando una mejor experiencia de usuario y rendimiento.
Esta biblioteca se basa en los Hooks de React que se introdujeron en la versión 16.8, proporcionando una forma más directa de trabajar con el estado del formulario y la lógica de validación.
Al usar React Hook Form, puedes esperar una reducción en la cantidad de código que tienes que escribir y un mejor manejo de la lógica de tu formulario. El resultado es un formulario más limpio, más fácil de leer, y que es más eficiente en términos de rendimiento.
Consideraciones y Requerimientos
Para este tutorial es requerido entender los Hooks de React, y tambien
Caracteristicas de React Hook Form
React Hook Form es una biblioteca que cuenta con un junto muy util de características. Aquí hay algunas de las características más importantes de esta biblioteca:
Menor cantidad de re-renderizados: React Hook Form minimiza la cantidad de re-renderizados del componente gracias a su diseño desacoplado.
Validación de formularios: Esta biblioteca soporta validación de formularios nativa y también se integra bien con librerías de validación populares como Yup, Zod, Superstruct, Joi y más.
Compatible con formularios controlados y no controlados: Esto significa que puedes controlar los componentes de tu formulario directamente o dejar que React Hook Form maneje eso por ti.
Hooks personalizados: React Hook Form proporciona hooks personalizados como
useForm
que simplifican aún más la gestión de formularios.Soporte de TypeScript: Si estás usando TypeScript en tu proyecto, te alegrará saber que React Hook Form tiene soporte completo para TypeScript.
Facilidad para obtener el estado del formulario: React Hook Form facilita la recopilación de datos de los formularios, proporcionando métodos para obtener el estado del formulario.
Estrategia de registro: Puedes elegir el momento de registro de los campos de tu formulario, lo que puede ayudar a optimizar el rendimiento de tu aplicación.
En este tutorial, te guiaré a través de los conceptos básicos de React Hook Form, cómo configurarlo y cómo usarlo para construir formularios eficientes en React.
Creacion de un Proyecto
Para este ejemplo vamos a crear un proyecto de React usando Vitejs, ejecuta lo siguiente:
npm create vite
con las siguientes opciones:
√ Project name: ... react-hook-form-tutorial
√ Select a framework: » React
√ Select a variant: » JavaScript
Luego ejecuta:
cd react-hook-form-tutorial
npm install
npm run dev
Luego instalaremos la biblioteca react-hook-form
usando:
npm i react-hook-form
Tambien puedes abrirlo en Visual Studio Code con el comando
code .
Creacion de un formulario
Luego vamos a crear un formulario basico que nos permite practicar con la biblioteca react-hook-form
. Aquí tienes un ejemplo básico:
Actualiza el archivo src/App.jsx
con el siguiente código:
function Formulario() {
return (
<form>
<label>Nombre:</label>
<input type="text" name="nombre" />
<label>Correo Electrónico:</label>
<input type="email" name="correo" />
<label>Contraseña:</label>
<input type="password" name="contrasena" />
<label>Confirma Contraseña:</label>
<input type="password" name="confirmaContrasena" />
<label>Fecha de Nacimiento:</label>
<input type="date" name="fechaNacimiento" />
<label htmlFor="pais">Pais:</label>
<select name="pais" id="pais">
<option value="mx">México</option>
<option value="co">Colombia</option>
<option value="ar">Argentina</option>
</select>
<input type="checkbox" name="aceptaTerminos" />
<label>Acepto los términos y condiciones</label>
<button type="submit">Enviar</button>
</form>
);
}
export default Formulario;
Luego para que luzca mejor el formulario, elimina el archivo src/App.css
. Y luego actualiza todo el codigo de src/index.css
con lo siguiente:
body {
background: #101010;
color: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
label {
display: block;
}
span {
display: block;
color: tomato;
font-size: x-small;
}
input, select {
margin-bottom: .3rem;
width: 100%;
}
Este código CSS básicamente hace lo siguiente:
El bloque
body
establece el color de fondo a un tono oscuro (#101010
), y el color del texto a blanco. Además, utiliza Flexbox para centrar horizontal y verticalmente su contenido en la pantalla. Elheight: 100vh;
asegura que el cuerpo ocupe la altura total de la ventana del navegador.El bloque
span
establece el color rojo, con un tamaño pequeño y un display block para que salte a la siguiente linea.El bloque
label
hace que cada etiqueta (<label>
) se muestre como un bloque. Esto significa que cada etiqueta ocupará su propia línea en la página, en lugar de fluir junto con otros elementos en línea.El bloque
input
agrega un margen inferior a cada entrada (<input>
). Esto proporciona un pequeño espacio entre cada entrada y el elemento que sigue, mejorando así la legibilidad y la apariencia del formulario.
register y handleSubmit
Cuando trabajamos con react-hook-form
una de los primeros pasos es registrar cada input que esperamos validar, y esta su Caracteristica mas notoria de la biblioteca, porque nos evita que creemos un estado para cada input, ademas de evitar que creemos una propiedad name
y manejemos su funcion onChange
. Esta biblioteca ya lo hace por nosotros.
Por ejemplo usando la funcion register nuestro codigo del formulario quedaria asi:
import { useForm } from "react-hook-form";
function Formulario() {
const { register } = useForm()
return (
<form>
<label>Nombre:</label>
<input type="text" name="nombre"
{...register("nombre")}
/>
<label>Correo Electrónico:</label>
<input type="email" name="correo"
{...register("correo")}
/>
<label>Contraseña:</label>
<input type="password" name="password"
{...register("password")}
/>
<label>Confirma Contraseña:</label>
<input type="password" name="confirmarPassword"
{...register("confirmarPassword")}
/>
<label>Fecha de Nacimiento:</label>
<input type="date" name="fechaNacimiento"
{...register("fechaNacimiento")}
/>
<label htmlFor="pais">Pais:</label>
<select name="pais" id="pais"
{...register("pais")}
>
<option value="mx">México</option>
<option value="co">Colombia</option>
<option value="ar">Argentina</option>
</select>
<input type="checkbox" name="aceptaTerminos"
{...register("aceptaTerminos")}
/>
<label>Acepto los términos y condiciones</label>
<button type="submit">Enviar</button>
</form>
);
}
export default Formulario;
Este código es una versión actualizada del formulario que se proporcionó antes, esta vez usando React Hook Form. Entre los principales cambios que podras notar estan los siguientes:
import { useForm } from "react-hook-form";
: Este importa la funciónuseForm
desde React Hook Form. La funciónuseForm
se utiliza para crear métodos y objetos que necesitarás para trabajar con tu formulario.const { register } = useForm();
: Aquí estamos desestructurandoregister
deuseForm
. Como mencionamos antes,register
se utiliza para "registrar" los campos de entrada con React Hook Form para que pueda manejar su estado y validarlos.{...register("nombre")}
: Este es un ejemplo de cómo registrar un campo con React Hook Form. En este caso, el camponombre
se está registrando. Esto se hace en todos los campos de entrada y select que deseas que React Hook Form maneje.Al final, cuando se envía el formulario, los valores de todos los campos registrados estarán disponibles para su uso a través del objeto devuelto por
useForm()
.
De hecho react-hook-form
tambien maneja el envio del formulario usando un metodo llamado handleSubmit
handleSubmit
handleSubmit es un metodo de que nos permite manejar el metodo onSubmit
del formulario de una forma muy sencilla, esto para poder recibir todos los datos de los inputs que hemos registrado, antes del envio.
Para ver de que se trata actualicemos el codigo de la siguiente forma:
const { register, handleSubmit } = useForm()
const onSubmit = handleSubmit(data => {
console.log(data)
})
return (
<form onSubmit={onSubmit}>
...
</form>
);
...
Entre las partes principales que podemso encontrar en este codigo podemos resaltar las siguiente:
const { register, handleSubmit } = useForm()
: Se está utilizando el hookuseForm()
de React Hook Form para obtener las funcionesregister
yhandleSubmit
.register
se usa para registrar los campos del formulario con React Hook Form, mientras quehandleSubmit
se usa para manejar la presentación del formulario.const onSubmit = handleSubmit(data => { console.log(data) })
: Aquí se está utilizandohandleSubmit
para crear una funciónonSubmit
. Esta función se invocará cuando el usuario envíe el formulario. La funciónhandleSubmit
toma como argumento una función de devolución de llamada que recibirá los datos del formulario como su argumento. En este caso, los datos del formulario se están imprimiendo en la consola.<form onSubmit={onSubmit}>
: Este es el formulario en sí. Cuando el usuario envíe este formulario, se invocará la funciónonSubmit
que definimos anteriormente.
Validaciones y Error
El modulo react-hook-form
tambien ofrece validaciones muy practicas que podemos añadir a los inputs, por ejemplo para decir que input solo permite determinada cantidad de caracteres, es requerido o es de tipo email este ya incluye un parametro para definir todo esto.
De hecho React Hook Form facilita la validación de formularios al alinearse con el estándar HTML existente para la validación de formularios.
Por ejemplo para validar el input "nombre" podemos hacerlo de esta forma:
<label>Nombre:</label>
<input type="text" name="nombre"
{...register("nombre", {
required: true,
maxLength: 20,
})}
/>
Si envias el formulario este no ejecutara el handleSubmit
al no ser valido.
Ademas relacionado a esto tambien la biblioteca nos proporciona un objeto llamado errors
el cual nos permite ver que input es el que ha dado error para que nosotros podamos colocar quizas algun mensaje o cambiar los estilos del input.
Para ver estos errores actuliza lo siguiente:
function Formulario() {
const { register, handleSubmit, formState: { errors } } = useForm()
console.log(errors)
...
Este fragmento de código lo que hace es lo siguiente:
const { register, handleSubmit, formState: { errors } } = useForm()
: Esta línea está usando el hookuseForm
de React Hook Form para obtener las funcionesregister
yhandleSubmit
, y el objetoerrors
dentro deformState
, en dondeerrors
contiene cualquier error de validación en tu formulario.
Así, cada vez que se vuelva a renderizar el componente Formulario
(por ejemplo, cuando el usuario interactúa con los campos del formulario), se imprimirá el objeto errors
actualizado en la consola. Esto puede ser útil para depurar tus reglas de validación y asegurarte de que están funcionando como se esperaba.
De hecho usando este objeto errors, podemos mostrar algun mensaje:
<input type="text" name="nombre"
{...register("nombre", {
required: true,
maxLength: 20,
})}
/>
{
errors.nombre && <span>Nombre es requerido</span>
}
El codigo de errors en esta ultima porcion de codigo, solo se ejecutara si el campo "nombre" no cumple con las validaciones del formulario.
Aunque nosotros tambien podemos obtener mas informacion si añadimos mas propiedad a las validaciones, como lo siguiente:
<input type="text" name="nombre"
{...register("nombre", {
required: {
value: true,
message: 'Nombre es requerido'
},
maxLength: 20,
})}
/>
{
errors.nombre && <span>{errors.nombre.message}</span>
}
Ahora cada errors puede fallar por mas condiciones que coloquemos asi que cada objeto tambien tiene un propiedad type
que ayuda a saber porque fallo, por ejemplo si actualizamos el input a lo siguiente:
<input type="text" name="nombre"
{...register("nombre", {
required: {
value: true,
message: 'Nombre es requerido'
},
maxLength: 20,
minLength: 2
})}
/>
{
errors.nombre && <span>Nombre requerido</span>
}
Este mostrara el mismo error tanto si colocamos dos datos, como si nos pasamos de 20, o si no colocamos nada. lo que no es bueno, porque cada error deberia mostrar un mensaje distinto, asi que lo que podemos hacer es considerar cada posible error de la siguiente forma:
<label>Nombre:</label>
<input type="text" name="nombre"
{...register("nombre", {
required: {
value: true,
message: 'Nombre es requerido'
},
maxLength: 20,
minLength: 2
})}
/>
{errors.nombre?.type === "required" && <span>Nombre requerido</span>}
{errors.nombre?.type === "maxLength" && <span>Nombre no debe ser mayor a 20 caracteres</span>}
{errors.nombre?.type === "minLength" && <span>Nombre debe ser mayor a 2 caracteres</span>}
Esto lanzara un error distinto para cada tipo, ya sea cuando no coloquemos nada, cuando nos pasemos la cantidad maxima de caracteres o cuando no coloquemos los suficientes caracteres.
pattern
Ahora en cuanto a validar emails, la biblioteca tambien posee un campo llamado pattern que permite validar esto facilmente, actualiza el input email para probarlo:
<label>Correo Electrónico:</label>
<input type="email" name="correo"
{...register("correo", {
required: {
value: true,
message: 'Correo es requerido'
},
pattern: {
value: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
message: 'Correo no válido'
}
})}
/>
{
errors.correo && <span>{errors.correo.message}</span>
}
Como puedes notar aqui el pattern
tiene una expresion regular, lo que comprueba que lo que escribamos en el input coincida. ademas tambien cuando mostramos errors este no tiene multiples validaciones, solo uno, y eso es porque si solo valido que errors.correo exista, mostrara su mensaje ya incluido en el mismo objeto errors.correo, que puede ser tanto si es required
o como si es pattern
validate
Ahora vamos a probar cuando necesitemos validar campos de una forma mas personalizada, para esto tenemos la propiedad Validate, que nos permite ejecutar nuestra propia logica. Provemoslo para poder validar que un usuario sea mayor de edad.
<label>Fecha de Nacimiento:</label>
<input type="date" name="fechaNacimiento"
{...register("fechaNacimiento", {
required: {
value: true,
message: 'Fecha de nacimiento es requerida'
},
validate: value => {
const fechaNacimiento = new Date(value);
const fechaActual = new Date();
const edad = fechaActual.getFullYear() - fechaNacimiento.getFullYear();
return edad >= 18 || 'Debes ser mayor de edad'
}
})}
/>
{
errors.fechaNacimiento && <span>{errors.fechaNacimiento.message}</span>
}
Lo unico importante a tener en cuenta es que la funcion validate, nos da un parametro que es value, que es lo que esta escribiendo el usuario, y la funcion solo debe retornar un true si todo esta bien o un false
si no es correcto, o en nuestro caso retornamos un string para poder mostrarlo en pantalla.
watch
Muchas veces cuando escribimos en un formulario quizas vamos a querer tener funcionalidades dinamicas, es decir que cuando escribamos en algo este ejecute otra logica, para eso tenemo dos funciones, uno es la funcion watch
y la otra es getValues
ambas nos devuelvene el valor de un campo del formulario, pero con una pequeña diferencia.
el campo watch
nos permite vigilar todos los campos. Para verlo añade el siguiente codigo:
const { register, handleSubmit, formState: { errors }, watch } = useForm()
const onSubmit = handleSubmit(data => {
console.log(data)
})
return (
<form onSubmit={onSubmit}>
...
<pre style={{width: "400px"}}>
{JSON.stringify(watch(), null, 2)}
</pre>
</form>
Esto lo que hara es que cualquier dato que escribas te permitira ver el valor actual de los datos.
Siendo util cuando queremos mostrar elementos condicionados como un input por ejemplo:
<label htmlFor="pais">Pais:</label>
<select name="pais" id="pais"
{...register("pais")}
>
<option value="mx">México</option>
<option value="co">Colombia</option>
<option value="ar">Argentina</option>
</select>
{
watch('pais') === 'ar' && <input type="text" placeholder="Provincia" {
...register("provincia", {
required: {
value: true,
message: 'Provincia es requerida'
}
})
} />
}
Mientra que getValue, no vigila ningun cambio, solo al ejecutarse devuelve los valores. Y aunque esto no parezca muy util, recuerda que hay veces que necesitaremos que un campo se ejecute en cada cambio de un input. Es decir el watch es un subscriber, mientras que el getValues no.
Ahora para probarlo vamos a usarlo para hacer una comparacion en las contraseñas:
const { register, handleSubmit, formState: { errors }, watch } = useForm()
const password = useRef(null);
password.current = watch("password", "");
...
return (
<form onSubmit={onSubmit}>
<label>Contraseña:</label>
<input type="password" name="password"
{...register("password", {
required: {
value: true,
message: 'Contraseña es requerida'
},
minLength: {
value: 6,
message: 'Contraseña debe ser mayor a 6 caracteres'
}
})}
/>
{
errors.password && <span>{errors.password.message}</span>
}
<label>Confirma Contraseña:</label>
<input type="password" name="confirmarPassword"
{...register("confirmarPassword", {
required: {
value: true,
message: 'Confirmar contraseña es requerida'
},
minLength: {
value: 6,
message: 'Confirmar contraseña debe ser mayor a 6 caracteres'
},
validate: value => value === password.current || 'Las contraseñas no coinciden'
})}
/>
{
errors.confirmarPassword && <span>{errors.confirmarPassword.message}</span>
}
File
tambien es posible usar las validaciones con archivos:
<label htmlFor="archivo">subir foto:</label>
<input type="file" {
...register("archivo", {
required: {
value: true,
message: 'Archivo es requerido'
}
})
} />
{
errors.archivo && <span>{errors.archivo.message}</span>
}
setValue
la funcion setValue tipicamente lo puedes usar cuando quieres crear un campo que no necesariamente exista cuando el formulario es creado. Por ejemplo tipicamente lo puedes usar cuando subes un archivo o quieras cambiar un valor:
const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm()
<label htmlFor="archivo">subir nombre de archivo:</label>
<input type="file" onChange={(e) => {
setValue("archivo", e.target.files[0].name)
} } />
{
errors.archivo && <span>{errors.archivo.message}</span>
}
defaultValues
Ademas de todas las funciones y validaciones algo muy comun tambien es colocar valores por defecto, para poder hacer esto tenemos el campo defaultValues, que funciona para establecer los valores iniciales que queramos ver en el formulario.
const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm({
defaultValues: {
nombre: 'jose perez',
correo: 'jose@gmail.com',
fechaNacimiento: '1900-07-05',
password: '123456',
confirmarPassword: '123456',
pais: 'ar',
archivo: '',
aceptaTerminos: false
}
})
reset
Esta funcion es la mas sencilla de todas, nos permite ejecutar el evento reset
del formulario, para limpiar todos los campos
const { register, handleSubmit, formState: { errors }, watch, setValue, reset } = useForm({
defaultValues: {
nombre: '',
correo: '',
fechaNacimiento: '',
password: '',
confirmarPassword: '',
pais: 'ar',
archivo: '',
aceptaTerminos: false
}
})
const onSubmit = handleSubmit(data => {
console.log(data)
// reset()
reset({
nombre: '',
correo: '',
fechaNacimiento: '',
password: '',
confirmarPassword: '',
pais: 'ar',
archivo: '',
aceptaTerminos: false
})
})
Formulario Final
Finalmente el formulario quedaria así:
import { useRef } from "react";
import { useForm } from "react-hook-form";
function Formulario() {
const {
register,
handleSubmit,
formState: { errors },
watch,
setValue,
reset,
} = useForm({
defaultValues: {
nombre: "",
correo: "",
fechaNacimiento: "",
password: "",
confirmarPassword: "",
pais: "co",
archivo: "",
aceptaTerminos: false,
},
});
const password = useRef(null);
password.current = watch("password", "");
const onSubmit = handleSubmit((data) => {
console.log(data);
// reset({
// nombre: '',
// correo: '',
// fechaNacimiento: '',
// password: '',
// confirmarPassword: '',
// pais: 'ar',
// archivo: '',
// aceptaTerminos: false
// })
reset();
});
return (
<form onSubmit={onSubmit}>
<label>Nombre:</label>
<input
type="text"
name="nombre"
{...register("nombre", {
required: {
value: true,
message: "Nombre es requerido",
},
maxLength: 20,
minLength: 2,
})}
/>
{errors.nombre?.type === "required" && <span>Nombre requerido</span>}
{errors.nombre?.type === "maxLength" && (
<span>Nombre no debe ser mayor a 20 caracteres</span>
)}
{errors.nombre?.type === "minLength" && (
<span>Nombre debe ser mayor a 2 caracteres</span>
)}
<label>Correo Electrónico:</label>
<input
type="email"
name="correo"
{...register("correo", {
required: {
value: true,
message: "Correo es requerido",
},
pattern: {
value: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
message: "Correo no válido",
},
})}
/>
{errors.correo && <span>{errors.correo.message}</span>}
<label>Fecha de Nacimiento:</label>
<input
type="date"
name="fechaNacimiento"
{...register("fechaNacimiento", {
required: {
value: true,
message: "Fecha de nacimiento es requerida",
},
validate: (value) => {
const fechaNacimiento = new Date(value);
const fechaActual = new Date();
const edad =
fechaActual.getFullYear() - fechaNacimiento.getFullYear();
return edad >= 18 || "Debes ser mayor de edad";
},
})}
/>
{errors.fechaNacimiento && (
<span>{errors.fechaNacimiento.message}</span>
)}
<label>Contraseña:</label>
<input
type="password"
name="password"
{...register("password", {
required: {
value: true,
message: "Contraseña es requerida",
},
minLength: {
value: 6,
message: "Contraseña debe ser mayor a 6 caracteres",
},
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<label>Confirma Contraseña:</label>
<input
type="password"
name="confirmarPassword"
{...register("confirmarPassword", {
required: {
value: true,
message: "Confirmar contraseña es requerida",
},
minLength: {
value: 6,
message: "Confirmar contraseña debe ser mayor a 6 caracteres",
},
validate: (value) =>
value === password.current || "Las contraseñas no coinciden",
})}
/>
{errors.confirmarPassword && (
<span>{errors.confirmarPassword.message}</span>
)}
<label htmlFor="pais">Pais:</label>
<select name="pais" id="pais" {...register("pais")}>
<option value="mx">México</option>
<option value="co">Colombia</option>
<option value="ar">Argentina</option>
</select>
{watch("pais") === "ar" && (
<input
type="text"
placeholder="Provincia"
{...register("provincia", {
required: {
value: true,
message: "Provincia es requerida",
},
})}
/>
)}
<label htmlFor="archivo">subir nombre de archivo:</label>
<input
type="file"
onChange={(e) => {
setValue("archivo", e.target.files[0].name);
}}
/>
{errors.archivo && <span>{errors.archivo.message}</span>}
<input
type="checkbox"
name="aceptaTerminos"
{...register("aceptaTerminos", {
required: {
value: true,
message: "Acepta los términos y condiciones",
},
})}
/>
<label>Acepto los términos y condiciones</label>
{errors.aceptaTerminos && <span>{errors.aceptaTerminos.message}</span>}
<button type="submit">Enviar</button>
<pre style={{ width: "400px" }}>{JSON.stringify(watch(), null, 2)}</pre>
<h3>Hello {watch("nombre")}</h3>
</form>
);
}
export default Formulario;