Saltar a contenido

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;