Formik
Formik es una biblioteca ligera de código abierto para React que simplifica la creación y manejo de formularios. Ayuda con las tres partes más tediosas de trabajar con formularios:
- Obtener valores dentro y fuera del estado del formulario.
- Validación y mensajes de error.
- Manejo del envío del formulario.
Patron de diseño
Formik implementa un enfoque de componente contenedor que encapsula la lógica del formulario y proporciona props y métodos auxiliares a los componentes hijos. Este patrón permite una separación clara entre la lógica del formulario y su presentación visual.
Los principales elementos de este patrón son:
- El componente Formik que actúa como contenedor y maneja el estado del formulario.
- Hooks como useFormik para manejar la lógica del formulario en componentes funcionales.
- Componentes como Field y ErrorMessage para renderizar campos de entrada y mensajes de error.
Este patrón permite una separación clara entre la lógica del formulario y su presentación visual, simplificando la creación de formularios complejos en React.
Requisitos previos
Instalación de dependencias
Primero, debemos instalar Formik y Yup, que se usarán para la gestión y validación de formularios.
npm install formik yup
npm install yup
Estas librerías facilitarán la creación de formularios dinámicos y sus validaciones.
Formulario básico con Formik
Vamos a modificar el componente Login.jsx
para gestionar el estado del formulario usando Formik.
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
const Login = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({email, password}, { setSubmitting }) => {
console.log(email, password)
try {
await login({ email, password });
console.log("user logged in");
} catch (error) {
console.log(error.code);
console.log(error.message);
}
finally {
setSubmitting(false)
}
}
return (
<>
<h1>Login</h1>
<Formik
initialValues={{email:"test@test.com", password:"123456"}}
onSubmit={onSubmit}
>
{
({values,handleChange, handleSubmit, isSubmitting})=> (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
/>
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
/>
<button type="submit" disabled={isSubmitting}>Login</button>
</form>
)
}
</Formik>
</>
);
};
export default Login;
Validation
El esquema de validación se define con Yup, permitiendo manejar requisitos como:
- Email válido.
- Contraseña con longitud mínima.
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
import * as Yup from "yup"
const Login = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({email, password}, { setSubmitting }) => {
console.log(email, password)
try {
await login({ email, password });
console.log("user logged in");
} catch (error) {
console.log(error.code);
console.log(error.message);
}
finally {
setSubmitting(false)
}
}
const validationSchema = Yup.object().shape({
email: Yup.string().trim().email("Email no válido").required(),
password: Yup.string().trim().min(6, "Mínimo 6 carácteres").required()
})
return (
<>
<h1>Login</h1>
<Formik
initialValues={{email:"test@test.com", password:"123456"}}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{
({values,handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur})=> (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
/>
{
errors.email && touched.email && errors.email
}
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
/>
{
errors.password && touched.password && errors.password
}
<button type="submit" disabled={isSubmitting}>Login</button>
</form>
)
}
</Formik>
</>
);
};
export default Login;
setErrors & resetForm
Ye estamos mostrando los errores propios de nuestra validación, pero lo ideal sería mostrar dentro de esos errores los que nos devuelve el backend, en este caso, Firebase. Formik proporciona métodos útiles para manejar errores y reiniciar formularios:
- setErrors: Permite configurar manualmente errores específicos.
- resetForm: Restablece el formulario a su estado inicial.
Ejemplo:
const onSubmit = (values, { setErrors, resetForm }) => {
if (values.email !== "test@example.com") {
setErrors({ email: "Correo no registrado" });
} else {
console.log("Formulario enviado:", values);
resetForm();
}
};
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
import * as Yup from "yup"
const Login = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({email, password}, { setSubmitting, setErrors, resetForm }) => {
console.log(email, password)
try {
await login({ email, password });
console.log("user logged in");
resetForm()
} catch (error) {
if (error.code === "auth/invalid-credential") {
return setErrors({ credentials: "Credenciales invalidas"});
}
}
finally {
setSubmitting(false)
}
}
const validationSchema = Yup.object().shape({
email: Yup.string().trim().email("Email no válido").required(),
password: Yup.string().trim().min(6, "Mínimo 6 carácteres").required()
})
return (
<>
<h1>Login</h1>
<Formik
initialValues={{email:"test@test.com", password:"123456"}}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{
({values,handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur})=> (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
/>
{
errors.email && touched.email && errors.email
}
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
/>
{
errors.password && touched.password && errors.password
}
<button type="submit" disabled={isSubmitting}>Login</button>
{
errors.credentials && <p>{errors.credentials}</p>
}
</form>
)
}
</Formik>
</>
);
};
export default Login;
Register
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { register } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
import * as Yup from "yup"
const Register = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({ email, password }, { setSubmitting, setErrors, resetForm }) => {
try {
await register({ email, password });
console.log("user registered");
resetForm();
} catch (error) {
console.log(error.code);
console.log(error.message);
if (error.code === "auth/email-already-in-use") {
return setErrors({ email: "El email ya está registrado" });
}
if (error.code === "auth/invalid-email") {
return setErrors({ email: "Email no válido" });
}
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().trim().email("Email no válido").required(),
password: Yup.string().trim().min(6, "Mínimo 6 carácteres").required()
});
return (
<>
<h1>Register</h1>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{
({values,handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur})=> (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
/>
{errors.password && touched.password && errors.password}
<button
type="submit"
disabled={isSubmitting}
>
Register
</button>
</form>
)}
</Formik>
</>
);
};
export default Register;
Material UI
Material UI es una biblioteca de componentes de interfaz de usuario para React que sigue los diseños de Material Design de Google. Proporciona componentes estilizados y listos para usar para acelerar el desarrollo de aplicaciones web y móviles. Permite a los desarrolladores crear interfaces de usuario atractivas y funcionales con facilidad, manteniendo consistencia en la apariencia y experiencia del usuario.
Para más información, consultar la documentación oficial de Material UI.
Instalación
Para comenzar a usar Material UI, se deben instalar los paquetes principales junto con fuentes y estilos adicionales.
npm install @mui/material @emotion/react @emotion/styled
npm install @fontsource/roboto
npm install @mui/icons-material
npm install @mui/lab # Para componentes experimentales como LoadingButton
Importamos las fuentes en el main.js
.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { router } from "./router";
import { RouterProvider } from "react-router-dom";
import UserProvider from "./context/UserContext";
ReactDOM.createRoot(document.getElementById("root")).render(
<UserProvider>
<RouterProvider router={router} />
</UserProvider >
);
CssBaseline
Material UI proporciona un componente llamado CssBaseline
que aplica un restablecimiento de estilos base para corregir inconsistencias entre navegadores y dispositivos. Esto mejora la compatibilidad y establece un punto de partida limpio.
main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { router } from "./router";
import { RouterProvider } from "react-router-dom";
import UserProvider from "./context/UserContext";
import { CssBaseline } from "@mui/material";
ReactDOM.createRoot(document.getElementById("root")).render(
<>
<CssBaseline />
<UserProvider>
<RouterProvider router={router} />
</UserProvider >
</>
);
Algunos recursos de Material UI
Ejemplo de Componente: Button
El componente Button
de Material UI permite implementar botones estilizados con múltiples variantes.
Uso básico:
import { Button } from "@mui/material";
<Button variant="contained">Hello World</Button>
Login con Material UI y Formik
A continuación, se presenta un ejemplo de un formulario de inicio de sesión utilizando Material UI, Formik
para manejar formularios y Yup
para la validación.
Login.jsx
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
import * as Yup from "yup"
import { Avatar, Box, Button, TextField, Typography } from "@mui/material";
import LoginIcon from '@mui/icons-material/Login';
import { LoadingButton } from "@mui/lab";
const Login = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({email, password}, { setSubmitting, setErrors, resetForm }) => {
console.log(email, password)
try {
await login({ email, password });
console.log("user logged in");
resetForm()
} catch (error) {
if (error.code === "auth/invalid-credential") {
return setErrors({ email: "Credenciales invalidas", password: "Credenciales invalidas"});
}
}
finally {
setSubmitting(false)
}
}
const validationSchema = Yup.object().shape({
email: Yup.string().trim().email("Email no válido").required("Email requerido"),
password: Yup.string().trim().min(6, "Mínimo 6 carácteres").required("Contraseña requerida")
})
return (
<Box sx={{mt:"1rem", maxWidth: "400px", mx:"auto", textAlign:"center"}}>
<Avatar sx={{mx: "auto", bgcolor:"#111"}}>
<LoginIcon/>
</Avatar>
<Typography
variant="h5"
component="h1"
>
Login
</Typography>
<Formik
initialValues={{email:"test@test.com", password:"123456"}}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{
({values,handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur})=> (
<Box
onSubmit={handleSubmit}
sx={{mt: 1}}
component={"form"}
>
<TextField
type="text"
placeholder="email@email.com"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
id="email"
label="Introduce el email"
fullWidth
sx={{mb: 3}}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
type="password"
placeholder="******"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
id="password"
label="Introduce la contraseña"
fullWidth
sx={{mb: 3}}
error={errors.password && touched.password}
helperText={errors.password && touched.password && errors.password}
/>
<LoadingButton
variant="contained"
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
sx={{mb: 3}}
>
Acceder
</LoadingButton>
<Button
fullWidth
component={Link}
to={"/register"}
>
¿No tienes cuenta? Registrate
</Button>
</Box>
)
}
</Formik>
</Box>
);
};
export default Login;
Este ejemplo muestra cómo combinar Formik
, Yup
, y Material UI para crear un formulario totalmente funcional con validación y diseño estilizado.
Registro de usuario
De forma similar al formulario de inicio de sesión, el formulario de registro utiliza Formik
y Yup
junto con componentes de Material UI.
Registro.jsx
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { register } from "../config/firebase";
import { UserContext } from "../context/UserContext";
import { Formik } from "formik";
import * as Yup from "yup"
import { Avatar, Box, Button, TextField, Typography } from "@mui/material";
import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import { LoadingButton } from "@mui/lab";
const Registro = () => {
const navigate = useNavigate();
const {user} = useContext(UserContext)
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async ({ email, password }, { setSubmitting, setErrors, resetForm }) => {
try {
await register({ email, password });
console.log("user registered");
resetForm();
} catch (error) {
console.log(error.code);
console.log(error.message);
if (error.code === "auth/email-already-in-use") {
return setErrors({ email: "El email ya está registrado" });
}
if (error.code === "auth/invalid-email") {
return setErrors({ email: "Email no válido" });
}
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().trim().email("Email no válido").required("Email requerido"),
password: Yup.string().trim().min(6, "Mínimo 6 carácteres").required("Contraseña requerida")
})
return (
<Box sx={{mt:"1rem", maxWidth: "400px", mx:"auto", textAlign:"center"}}>
<Avatar sx={{mx: "auto", bgcolor:"#111"}}>
<AppRegistrationIcon/>
</Avatar>
<Typography
variant="h5"
component="h1"
>
Registro
</Typography>
<Formik
initialValues={{email:"test@test.com", password:"123456"}}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{
({values,handleChange, handleSubmit, isSubmitting, errors, touched, handleBlur})=> (
<Box
onSubmit={handleSubmit}
sx={{mt: 1}}
component={"form"}
>
<TextField
type="text"
placeholder="email@email.com"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
id="email"
label="Introduce el email"
fullWidth
sx={{mb: 3}}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
type="password"
placeholder="******"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
id="password"
label="Introduce la contraseña"
fullWidth
sx={{mb: 3}}
error={errors.password && touched.password}
helperText={errors.password && touched.password && errors.password}
/>
<LoadingButton
variant="contained"
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
sx={{mb: 3}}
>
Enviar
</LoadingButton>
<Button
fullWidth
component={Link}
to={"/login"}
>
Ya tienes cuenta? Accede
</Button>
{
errors.credentials && <p>{errors.credentials}</p>
}
</Box>
)
}
</Formik>
</Box>
);
};
export default Registro;