DEV Community

Cover image for React Router Data Mode: Parte 6 – Actions, formularios y mutaciones
Kevin Julián Martínez Escobar
Kevin Julián Martínez Escobar

Posted on • Edited on

React Router Data Mode: Parte 6 – Actions, formularios y mutaciones

Continuamos con la sexta entrega de esta serie sobre React Router Data Mode. En esta ocasión hablaremos sobre las actions: una de las piezas clave para el manejo de datos que nos ofrece React Router.


Si vienes del post anterior, puedes continuar con tu proyecto tal cual. Pero si prefieres empezar limpio o asegurarte de estar en el punto exacto, ejecuta los siguientes comandos:

# Enlace del repositorio https://github.com/kevinccbsg/react-router-tutorial-devto
git reset --hard
git clean -d -f
git checkout 05-action-create-contact
Enter fullscreen mode Exit fullscreen mode

¿Qué es una action?

Una action es una función que podemos asociar a una ruta, y que se encarga de realizar mutaciones de datos. Lo interesante es que, al completarse, React Router revalida automáticamente los datos de la interfaz sin que tengamos que manejar ese proceso manualmente.

En nuestro proyecto de contactos, vamos a comenzar por crear un nuevo contacto usando una action. Verás que al hacerlo, el listado se actualizará sin necesidad de escribir lógica adicional.


Manejo de formularios

En src/pages/ContactForm.tsx reemplazamos la etiqueta <form> por el componente <Form> de react-router, que es el encargado de disparar la action correspondiente al hacer submit.

Además, es importante indicar el método (por ejemplo, POST):

<Form className="space-y-4" method="POST">
Enter fullscreen mode Exit fullscreen mode

Este componente ejecutará la action definida en la ruta correspondiente. Para ello, configuramos la action en src/AppRoutes.tsx.

Un ejemplo tomado de la documentación oficial sería:

import { createBrowserRouter } from "react-router";
import { someApi } from "./api";

let router = createBrowserRouter([
  {
    path: "/projects/:projectId",
    Component: Project,
    action: async ({ request }) => {
      let formData = await request.formData();
      let title = formData.get("title");
      let project = await someApi.updateProject({ title });
      return project;
    },
  },
]);
Enter fullscreen mode Exit fullscreen mode

Al igual que hicimos con los loaders, lo recomendable es extraer la action a un archivo aparte.

Creamos src/pages/actions.tsx con lo siguiente:

import { ActionFunctionArgs } from "react-router";

export const newContactAction = async ({ request }: ActionFunctionArgs) => {
  console.log('Llamando a la action');
  return null;
};
Enter fullscreen mode Exit fullscreen mode

Esta función recibe un objeto con ActionFunctionArgs. Nos centraremos en request, aunque también podemos acceder a los params de la ruta.

Nota: una action siempre debe retornar algo, ya sea null o una redirección (que veremos más adelante).

En src/AppRoutes.tsx, configuramos la action en la ruta contacts/new así:

{
  path: "contacts/new",
  action: newContactAction,
  Component: ContactForm,
}
Enter fullscreen mode Exit fullscreen mode

Al probar el formulario en la web, deberías ver el console.log en tu consola, indicando que la action se ejecutó correctamente.


Obteniendo datos del formulario

Usamos request.formData() para capturar los datos, gracias a que nuestros inputs ya están bien etiquetados con name.

const formData = await request.formData();
const newContact= {
  firstName: formData.get('firstName') as string,
  lastName: formData.get('lastName') as string,
  username: formData.get('username') as string,
  email: formData.get('email') as string,
  phone: formData.get('phone') as string,
  avatar: formData.get('avatar') as string || undefined,
};
Enter fullscreen mode Exit fullscreen mode

Además, validamos que el método sea POST:

const method = request.method.toUpperCase();
if (method === 'POST') {
  // create the contact
}
return null;
Enter fullscreen mode Exit fullscreen mode

Action final

El código completo de nuestra action quedaría así:

import { ActionFunctionArgs } from "react-router";
import { createContact } from "@/api/contacts";

interface NewContact {
  firstName: string;
  lastName: string;
  username: string;
  email: string;
  phone: string;
  avatar?: string;
}

export const newContactAction = async ({ request }: ActionFunctionArgs) => {
  const formData = await request.formData();
  const method = request.method.toUpperCase();

  if (method === 'POST') {
    const newContact: NewContact = {
      firstName: formData.get('firstName') as string,
      lastName: formData.get('lastName') as string,
      username: formData.get('username') as string,
      email: formData.get('email') as string,
      phone: formData.get('phone') as string,
      avatar: formData.get('avatar') as string || undefined,
    };
    await createContact(newContact);
  }

  return null;
};
Enter fullscreen mode Exit fullscreen mode

Al crear un nuevo contacto, este se mostrará automáticamente en el sidebar.


Redirección tras crear

Ahora vamos a mejorar la experiencia redirigiendo directamente a la página del nuevo contacto. Usamos el helper redirect de react-router justo después de crear el contacto:

import { ActionFunctionArgs, redirect } from "react-router";
import { createContact } from "@/api/contacts";

interface NewContact {
  firstName: string;
  lastName: string;
  username: string;
  email: string;
  phone: string;
  avatar?: string;
}

export const newContactAction = async ({ request }: ActionFunctionArgs) => {
  const formData = await request.formData();
  const method = request.method.toUpperCase();

  if (method === 'POST') {
    const newContact: NewContact = {
      firstName: formData.get('firstName') as string,
      lastName: formData.get('lastName') as string,
      username: formData.get('username') as string,
      email: formData.get('email') as string,
      phone: formData.get('phone') as string,
      avatar: formData.get('avatar') as string || undefined,
    };
    const newContactResponse = await createContact(newContact);
    return redirect(`/contacts/${newContactResponse.id}`);
  }

  return null;
};
Enter fullscreen mode Exit fullscreen mode

Gestión del loading

Para gestionar el estado del formulario mientras se está enviando o cargando, usamos el hook useNavigation que nos ofrece React Router. Este hook nos da información sobre la navegación actual, y lo aprovechamos para saber si estamos enviando el formulario o esperando que se actualice la UI después del redirect.

const navigation = useNavigation();
Enter fullscreen mode Exit fullscreen mode

Con esto tenemos acceso al estado de la navegación actual, que puede ser "idle", "submitting" o "loading".

Luego creamos dos variables para detectar si estamos justo enviando este formulario en particular (no cualquier otro) o si estamos esperando que se recargue la vista tras completarse:

const isSubmitting = navigation.state === 'submitting' && navigation.formAction === '/contacts/new';
const isLoading = navigation.state === 'loading' && navigation.formAction === '/contacts/new';
Enter fullscreen mode Exit fullscreen mode

Esto es importante porque puede haber otras rutas o formularios en la app, y no queremos que se deshabilite el botón si no es este el formulario que se está usando.

Por último, combinamos ambos estados para saber si el botón debe estar deshabilitado:

const disabled = isSubmitting || isLoading;
Enter fullscreen mode Exit fullscreen mode

Así evitamos que el usuario envíe múltiples veces mientras el contacto se está creando o la UI se está actualizando.

Este sería el resultado:

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Form, useNavigation } from 'react-router';

const ContactForm = () => {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting' && navigation.formAction === '/contacts/new';
  const isLoading = navigation.state === 'loading' && navigation.formAction === '/contacts/new';
  const disabled = isSubmitting || isLoading;
  return (
    <div className="max-w-md mx-auto">
      <h1 className="text-2xl font-bold mb-4">Create New Contact</h1>
      <Form className="space-y-4" method="POST">
        {/* tus campos */}
        <Button type="submit" disabled={disabled }>
          {disabled ? 'Creating...' : 'Create Contact'}
        </Button>
      </Form>
    </div>
  );
};

export default ContactForm;
Enter fullscreen mode Exit fullscreen mode

Con esto conseguimos una mejor UX y evitamos múltiples submits innecesarios.


Conclusión

Hasta aquí esta sexta parte, donde hemos creado una action, manejado el formulario, redirigido tras el submit y controlado el loading.

Quizá te preguntes: ¿qué pasa con la validación? ¿y si tengo varias actions en una misma página, como editar o borrar contactos?

Todo eso lo veremos en la próxima entrega: cómo gestionar múltiples actions, validaciones y acciones que no dependen de un <Form>.

¡Nos vemos en la siguiente!

Top comments (0)