Como validar formularios en React con Formik y Styled Components
Crear y validar formularios en React puede llegar a ser bastante complejo. Tienes que encargarte de guardar los valores en el estado, de la validación de los datos y de mostrar los mensajes de error. Además, también tienes que gestionar el envío del formulario.
Afortunadamente, las librerías como Formik pueden ayudar a reducir la complejidad bastante para que puedas configurar un formulario mucho más rápido. Y esto es lo que aprenderás en este tutorial: cómo validar un formulario con Formik y aplicarle estilos con styled-components.
La Solución Final
Si quieres aprender como crear el formulario de este tutorial sigue leyendo. Si quieres ver la solución final para investigar por ti mismo como funciona todo, puedes encontrar una demo en Codesandbox o el repositorio en Github.
Configuración e Instalación
La manera más fácil de empezar es creando un nuevo proyecto desde 0 con
create-react-app. Puedes hacerlo ejecutando en tu terminal
npx create-react-app <your-projects-name>
o solo npx create-react-app .
para
instalar todo en la carpeta actual, si esta vacia.
Después, tienes que instalar Formik, Yup (para la validación del formulario) y styled-components.
Para instalar todos los paquetes ejecuta el siguiente comando en tu terminal:
npm install formik yup styled-components
Una vez tienes todo instalado, puedes empezar ejecutando npm start
en tu
terminal.
Creando el Formulario
Formik nos ofrece muchas opciones para controlar los formularios. Puedes usar todas las herramientas de ayuda incluidas con la librería —que es lo que yo hecho en este tutorial—, o puedes escribir todo a mano desde 0 y conectar el formulario y los sus campos de entrada de datos con los controladores de eventos de Formik.
Creo que la manera más fácil es usar todas las opciones de ayuda que nos ofrece porque ayuda a reducir mucho la complejidad de gestionar un formulario.
Para empezar, tienes que importar los componentes de Formik que necesitarás para renderizar el formulario y los campos de entrada de datos.
import { Formik, Field, Form, ErrorMessage } from "formik";
Lo más básico que necesitas para empezar es usar el componente <Formik />
que
se encarga de controlar la validación de los datos y el envío del formulario.
Después, necesitarás los componentes <Form />
y <Field />
para renderizar el
formulario y sus campos.
import React from "react";
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";
function App() {
return (
<div>
<h1>React form validation with formik and styled components</h1>
<Formik
initialValues={{
fullname: "",
email: "",
}}
validationSchema={Yup.object().shape({
fullname: Yup.string()
.min(2, "Your name is too short")
.required("Please enter your full name"),
email: Yup.string()
.email("The email is incorrect")
.required("Please enter your email"),
})}
onSubmit={(values, { setSubmitting }) => {
const timeOut = setTimeout(() => {
console.log(values);
setSubmitting(false);
clearTimeout(timeOut);
}, 1000);
}}
>
{({
values,
errors,
touched,
handleSubmit,
isSubmitting,
validating,
valid,
}) => {
return (
<Form name="contact" method="post" onSubmit={handleSubmit}>
<label htmlFor="fullname">
Fullname
<Field
type="text"
name="fullname"
autoComplete="name"
placeholder="your fullname"
/>
</label>
{errors.fullname && touched.fullname && <p>{errors.fullname}</p>}
<label htmlFor="email">
Email
<Field
type="email"
name="email"
autoComplete="email"
placeholder="your email"
/>
</label>
<ErrorMessage name="email">{(msg) => <p>{msg}</p>}</ErrorMessage>
<button type="submit" disabled={!valid || isSubmitting}>
{isSubmitting ? `Submiting...` : `Submit`}
</button>
</Form>
);
}}
</Formik>
</div>
);
}
export default App;
Con esto, tenemos lo básico para empezar a crear un formulario de contacto creado con Formik y React. Pero primero, vamos a ver que tenemos hasta ahora.
<Formik
initialValues={{
fullname: "",
email: "",
}}
validationSchema={Yup.object().shape({
fullname: Yup.string()
.min(2, "Your name is too short")
.required("Please enter your full name"),
email: Yup.string()
.email("The email is incorrect")
.required("Please enter your email"),
})}
onSubmit={(values, { setSubmitting }) => {
const timeOut = setTimeout(() => {
console.log(values);
setSubmitting(false);
clearTimeout(timeOut);
}, 1000);
}}
>
...
initialValues
es un objeto que guarda todos los valores iniciales de los
campos del formulario. En este ejemplo, estos son fullname
y email
, pero
puedes añadir los que necesites.
validationSchema
es un objeto que guarda el esquema de validación de Yup que
hemos definido con todas las reglas de validación de cada campo del formulario.
En este caso, tenemos un campo llamado fullname
de tipo texto con una longitud
mínima de 2 caracteres que también es un campo requerido. Hemos definido estos
requisitos usando los métodos Yup.string()
.
Si quieres ver todas las opciones que el validador de esquemas Yup tiene, puedes
leer su documentación. Finalmente, el
controlador de eventos onSubmit
se usa para controlar el envío del formulario.
El componente Formik usa el método render props para renderizar el formulario
y los campos de este. Devuelve varias variables y controladores de eventos que
podemos añadir al formulario HTML para controlarlo. Para hacerlo solo tienes que
añadir los controladores de eventos onSubmit
y onChange
proporcionados por
Formik, y usar el componente Field en vez de los elementos HTML del tipo input.
Después, una vez el usuario comience a introducir datos, se ejecutará la validación de los datos en cada campo.
Añadiendo Estilos al Formulario
Hasta ahora podemos renderizar el formulario y sus campos, pero aun no hemos
añadido los estilos. Puedes usar tus estilos CSS si lo prefieres, pero si
quieres seguir con el tutorial copia estos estilos que he usado yo, en un nuevo
archivo llamado styles.js
e importalo en el fichero App.js
Una vez hecho esto, tu fichero styles.js
debería ser como el siguiente:
import styled, { css } from "styled-components";
import { Field } from "formik";
export const PageWrapper = styled.section`
&,
& * {
box-sizing: border-box;
display: block;
}
hr {
display: block;
border: none;
border-top: 1px solid lightgrey;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
font-family: system-ui;
font-size: 1rem;
line-height: 1.5rem;
max-width: 35em;
margin-left: auto;
margin-right: auto;
margin-top: 1.5rem;
padding: 1rem 0.75rem;
border: 1px solid lightgrey;
border-radius: 4px;
`;
export const CodeWrapper = styled.pre`
font-family: monospace;
font-size: 0.875rem;
line-height: 1.25rem;
background-color: hsl(210, 4%, 96%);
overflow: auto;
padding: 0.75rem;
margin: 0;
border-radius: 4px;
& strong {
margin-top: 1.5rem;
&:first-child {
margin-top: 0;
}
}
`;
export const Title = styled.h1`
font-size: 1rem;
line-height: 1.25rem;
margin-top: 0;
`;
export const Label = styled.label`
margin-top: 1.5rem;
width: 100%;
`;
export const Input = styled(Field)`
background-color: white;
border: 1px solid lightgrey;
border-radius: 4px;
font-size: 1rem;
line-height: 1.5rem;
font-style: normal;
font-weight: 400;
width: 100%;
margin-top: 0.5rem;
padding: 0.75rem 0.75rem;
&:focus,
&:active {
box-shadow: rgb(210, 213, 217) 0px 0px 2px 1px, rgb(227, 230, 232) 0px 0px 0px
3px;
border: 1px solid rgb(26, 33, 43);
outline: none;
}
/* Autocomplete styles in Chrome*/
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
background-color: white;
border: 1px solid lightgrey;
box-shadow: 0 0 0px 1000px #fff inset;
-webkit-box-shadow: 0 0 0px 1000px #fff inset;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: black;
}
${({ valid }) =>
valid &&
css`
border: 1px solid rgb(0, 156, 38);
&:focus,
&:active {
border: 1px solid rgb(0, 156, 38);
box-shadow: rgb(106, 237, 97) 0px 0px 2px 1px, rgb(177, 247, 160) 0px 0px
0px 3px;
outline: none;
}
/* Autocomplete styles in Chrome*/
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
border: 1px solid rgb(0, 156, 38);
}
`}
${({ error }) =>
error &&
css`
border: 1px solid rgb(191, 49, 12);
outline: none;
&:focus,
&:active {
box-shadow: rgb(244, 129, 116) 0px 0px 2px 1px, rgb(251, 178, 174) 0px 0px
0px 3px;
border: 1px solid rgb(191, 49, 12);
outline: none;
}
/* Autocomplete styles in Chrome*/
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
border: 1px solid rgb(191, 49, 12);
}
`}
`;
export const StyledInlineErrorMessage = styled.div`
background-color: rgb(255, 245, 245);
color: rgb(120, 27, 0);
display: block;
padding: 0.5rem 0.75rem;
margin-top: 0.5rem;
white-space: pre-line;
`;
export const Submit = styled.button`
width: 100%;
margin-top: 1.5rem;
background-color: rgb(24, 81, 187);
display: block;
text-align: center;
font-size: 1rem;
line-height: 1.5rem;
font-style: normal;
font-weight: 700;
height: 3rem;
white-space: nowrap;
color: rgb(232, 243, 255) !important;
padding: 0.5rem 1rem;
&:active,
&:focus,
&:hover {
cursor: pointer;
}
&:disabled {
cursor: pointer;
background-color: rgb(163, 168, 173);
box-shadow: none;
color: rgb(255, 255, 255) !important;
&:hover,
&:focus {
cursor: not-allowed;
}
}
`;
Y el archivo App.js
debería ser como este:
import React, { useState } from "react";
import { Formik, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import {
PageWrapper,
Title,
Label,
Input, StyledInlineErrorMessage,
Submit,
CodeWrapper,
} from "./styles";
function App() {
const [formValues, setFormValues] = useState();
return (
<PageWrapper>
<Title>
React form validation with formik and styled components demo
</Title>
<hr />
<Formik
initialValues={{
fullname: "",
email: "",
}}
validationSchema={Yup.object().shape({
fullname: Yup.string()
.min(2, "Your name is too short")
.required("Please enter your full name"),
email: Yup.string()
.email("The email is incorrect")
.required("Please enter your email"),
})}
onSubmit={(values, actions) => {
console.log(values);
setFormValues(values);
const timeOut = setTimeout(() => {
actions.setSubmitting(false);
clearTimeout(timeOut);
}, 1000);
}}
>
{({
values,
errors,
touched,
handleSubmit,
isSubmitting,
validating,
valid,
}) => {
return (
<>
<Form name="contact" method="post" onSubmit={handleSubmit}>
<Label htmlFor="fullname">
Fullname
<Input
type="text"
name="fullname"
autoCorrect="off"
autoComplete="name"
placeholder="your fullname"
valid={touched.fullname && !errors.fullname}
error={touched.fullname && errors.fullname}
/>
</Label>
{errors.fullname && touched.fullname && (
<StyledInlineErrorMessage>
{errors.fullname}
</StyledInlineErrorMessage>
)}
<Label htmlFor="email">
Email
<Input
type="email"
name="email"
autoCapitalize="off"
autoCorrect="off"
autoComplete="email"
placeholder="your email"
valid={touched.email && !errors.email}
error={touched.email && errors.email}
/>
</Label>
<ErrorMessage name="email">
{(msg) => (
<StyledInlineErrorMessage>{msg}</StyledInlineErrorMessage>
)}
</ErrorMessage>
<Submit type="submit" disabled={!valid || isSubmitting}>
{isSubmitting ? `Submiting...` : `Submit`}
</Submit>
</Form>
<hr />
<CodeWrapper>
<strong>Errors:</strong> {JSON.stringify(errors, null, 2)}
<strong>Touched:</strong> {JSON.stringify(touched, null, 2)}
{formValues && <strong>Submitted values:</strong>}
{JSON.stringify(formValues, null, 2)}
</CodeWrapper>
</>
);
}}
</Formik>
</PageWrapper>
);
}
export default App;
Los principales cambios que hemos hecho han sido usar un componente llamado
<Input />
que es el componente <Field />
de Formik, pero con estilos
aplicados con styled-components. Además, ahora lo importamos del fichero
styles.js
en vez de la librería Formik como antes.
Mostrando Mensajes de Estado
Para mostrar los mensajes de error, la librería Formik nos ofrece varias maneras de hacerlo.
Primero, puedes usar el componente <ErrorMessage />
que espera un atributo
name
con el valor de un campo existente en el formulario. Este atributo tiene
que ser igual al que hemos usado en el formulario, en el objecto de valores
iniciales y en el de validación proporcionado a Yup.
<ErrorMessage name="email">
{(msg) => <StyledInlineErrorMessage>{msg}</StyledInlineErrorMessage>}
</ErrorMessage>
La otra manera de hacerlo es usar el objeto errors
devuelto por el componente
<Formik />
en los render props.
{
errors.fullname && touched.fullname && (
<StyledInlineErrorMessage>{errors.fullname}</StyledInlineErrorMessage>
);
}
Con estos cambios, ahora estamos mostrando los mensajes de error si la validación no se cumple en algúno de los campos del formulario.
Además, para cambiar los estilos de los campos cuando son validos o no, usamos las variables tipo prop enviadas a styled-components para cambiar los estilos CSS.
<Label htmlFor="fullname">
Fullname
<Input
type="text"
name="fullname"
autoCorrect="off"
autoComplete="name"
placeholder="your fullname"
valid={touched.fullname && !errors.fullname}
error={touched.fullname && errors.fullname}
/>
</Label>
Después, dentro del componente styled-components podemos cambiar los estilos
cuando los valores valid
o error
sean true
.
${({ error }) =>
error &&
css`
border: 1px solid rgb(191, 49, 12);
outline: none;
&:focus,
&:active {
box-shadow: rgb(244, 129, 116) 0px 0px 2px 1px,
rgb(251, 178, 174) 0px 0px 0px 3px;
border: 1px solid rgb(191, 49, 12);
outline: none;
}
/* Autocomplete styles in Chrome*/
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
border: 1px solid rgb(191, 49, 12);
}
`}
Estos estilos CSS solo cambian el color de los bordes y el valor de los
box-shadow
para que sea más obvio para el usuario cuando el campo sea valido o
no.
Pero si intentas introducir algún valor en los campos, verás el siguiente error en la consola de tu navegador:
index.js:1375 Warning: Received `true` for a non-boolean attribute `valid`.
If you want to write it to the DOM, pass a string instead: valid="true" or valid={value.toString()}.
in input (created by FieldInner)
in FieldInner (created by Context.Consumer)
in FormikConnect(FieldInner) (created by Context.Consumer)
Filtrando Campos Que No Son Atributos Estándar HTML
Este error ocurre porque cuando usamos la sintaxis styled.div
,
styled-components solo pasa a los componentes los atributos HTML estándar. Pero
cuando se utiliza la sintaxis styled(ComponentName)
, pasa todos los atributos,
incluso si no son atributos HTML válidos. Es por eso por lo que React nos
advierte que estamos pasando atributos no válidos al elemento DOM.
Para solucionarlo, podemos usar un componente intermedio que he llamado
<FilteredPropsInputField />
(no se me ocurrió un nombre mejor 🤷♂️) que captura
todos los atributos pasados y solo muestra en el nodo DOM los que sean atributos
HTML válidos.
import React from "react";
import { Field } from "formik";
function FilteredPropsInputField({ className, valid, error, ...props }) {
return <Field className={className} {...props} />;
}
export default FilteredPropsInputField;
En nuestro caso, dado que solo estamos pasando los atributos valid
yerror
que no sean atributos HTML estándar, los desestructuramos junto con el
className
y los demás atributos recibidos. Luego, solo adjuntamos className
y props
al componente Field
para que no reciba ningún atributo que no forme
parte del estándar HTML.
Ahora, en lugar de usar un componente styled(Field)
en el archivo styles.js
,
usamos uno llamado styled(FilteredPropsInputField)
.
import FilteredPropsInputField from "./FilteredPropsInputField";
...
export const Input = styled(FilteredPropsInputField)`
background-color: white;
border: 1px solid lightgrey;
...
${({ valid }) =>
valid &&
css`
border: 1px solid rgb(0, 156, 38);
...
${({ error }) =>
error &&
css`
border: 1px solid rgb(191, 49, 12);
outline: none;
&:focus,
&:active {
...
Con estos cambios, ahora tenemos un formulario creado con Formik y styled-components 🎉.
Conclusiones
Ahora tu también deberías poder crear tu formulario en React fácilmente. Cuando empecé a crear el formulario de contacto de esta página no estaba seguro del todo como usar todos los componentes de la librería Formik. Lo que más me costo fue entender como puedo validar cada campo del formulario individualmente cuando tengan un error de validación o sean validos y no estaba seguro del todo como hacerlo. Pero una vez entendí como combinar los estilos de styled-components con el componente usado para eliminar los atributos no estándar HTML, fue bastante fácil crear el formulario.