Actions y loaders en React Router

#React-Router
Tabla de contenidos
  1. Server Loader
  2. Server Action
  3. Client Loader
  4. Client Action
  5. Conclusión

En React Router existen algunas funciones que puedes exportar o definir en tus archivos de rutas que te ayudarán mucho en tus aplicaciones web; estas funciones son de gran ayuda para manejar la información que se manda al cliente o la que se recibe en el servidor.

Primero que nada, me gustaría aclarar que estas funciones son exclusivas del modo framework y del modo data. Como bien es sabido, React Router nació como una librería de enrutamiento que, con el paso de los años, fue evolucionando y agregando diferentes features o ideas. Desde la versión 7 de React Router salió el framework mode, ya que este nuevo modo sería la sucesión de Remix (v1 y v2). Por lo que desde un inicio del framework mode se pueden usar estas funciones (loaders y actions), y para el data mode desde la versión 6.4.

Server Loader

Empecemos hablando de lo que son los loaders. Estaré enfocado exclusivamente en el framework mode, pero ten en cuenta que es muy similar en el data mode. Un loader es una función que exportas en tus archivos de ruta (ten muy en cuenta que exportas esta función solo en un archivo dentro de routes, ya que fuera de estos archivos simplemente será una función más). Esta función se ejecuta exclusivamente en el servidor, por lo cual los usuarios en el navegador nunca verán nada de ella y puedes escribir cualquier código que sea compatible con Node.js, ya que se ejecuta usándolo; por lo tanto, el código exclusivo del frontend, como la variable global window, causará un error si decides usar APIs exclusivas de la web.

En la función loader puedes enviar cualquier información que creas necesaria para que el usuario vea. Aquí es muy común hacer, por ejemplo, llamadas a una API o a la base de datos; se obtiene un registro o una lista de registros para enviar al frontend y que este pueda mostrarlos. Como puedes imaginar, esta función es la primera que se ejecuta al entrar a alguna ruta, y después se manda la interfaz de usuario (UI) por streaming.

Aquí te muestro un ejemplo:

// routes/product.$pid.tsx
import type { Route } from "./+types/product.$id";
import { data } from "react-router";
import { fakeDb } from "../db";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await fakeDb.getProduct(params.pid);
  if (!product) {
    return data("Not found", { status: 404 });
  }
  return product;
}

export default function Product({
  loaderData,
}: Route.ComponentProps) {
  const { name, description } = loaderData;
  return (
    <div>
      <h1>{name}</h1>
      <p>{description}</p>
    </div>
  );
}

Ejemplo obtenido de la documentación.

Como puedes ver, en la función ´loader obtienes la data, y la función que exportas por defecto (´default´) será la UI que se mostrará al entrar a esa ruta.

Server Action

Los actions son funciones muy similares a los loaders, es decir, también se ejecutan exclusivamente en el servidor. La diferencia radica en que el loader se ejecuta primero y, una vez que se ha mostrado toda la UI al usuario, si este realiza una acción (por ejemplo, enviar un formulario), es ahí donde se llama al action. Viéndolo de otra forma, los loaders equivalen a peticiones GET y los actions a todos los demás tipos (POST, PUT, PATCH, DELETE).

Un dato sobre esto es que la forma en que se separa lo que va al frontend y lo que se queda en el backend es gracias a Vite. Inicialmente React Router (Remix) tenía su propia librería que se encargaba de muchas tareas internas, pero una vez que se adaptó a usar Vite, este facilitó mucho el trabajo realizado, por lo que gran parte del esfuerzo y facilidad actual se debe a Vite.

Veamos el código:

// routes/project.$pid.tsx
import type { Route } from "./+types/project";
import { Form, data } from "react-router";
import { fakeDb } from "../db";

export async function action({
  request,
  params
}: Route.ActionArgs) {
  const pid = params.uid;
  if (!pid) {
    return data("Not found", { status: 404 });
  }
  const formData = await request.formData();
  const title = formData.get("title");
  const project = await fakeDb.updateProject(pid, { title });
  return project;
}

export default function Project({
  actionData,
}: Route.ComponentProps) {
  return (
    <div>
      {actionData ? (
        <p>{actionData.title} updated</p>
      ) : null}
      <h1>Project</h1>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">Submit</button>
      </Form>
    </div>
  );
}

Ya hemos visto estas dos funciones del lado del servidor, pero existen otras dos del lado del cliente que veremos a continuación.

Client Loader

La funcionea clientLoader son funciones que se ejecutan después del server loader y antes de hidratar la aplicación, por lo que podría decirse que funcionan como una pre-ejecución antes de mostrar la UI.

Por lo general, se usan cuando requieres validar ciertas cosas antes de mostrar la interfaz, normalmente porque quizás dependes de una API del navegador, por ejemplo, si necesitas llamar a la variable global ´window o asegurarte desde qué dispositivo accede el usuario (con la API del navegador es más fácil esta validación, ya que desde el backend es un poco más complejo). También puedes hacer alguna llamada a una API externa o librería que, por cualquier razón extraña, no pudiste usar en el server loader.

En resumen, aquí puedes enviar más datos al usuario o redirigirlo a algún otro lado antes de mostrar algo. Tiene su utilidad, pero no se suele usar tanto como los server loaders.

// routes/product.$pid.tsx
import type { Route } from "./+types/product.$id";

export async function clientLoader({
  params,
}: Route.ClientLoaderArgs) {
  // llamar a la api desde el lado del cliente
  // esto es raro pero es posible
  const res = await fetch(`/api/products/${params.pid}`);
  const product = await res.json();
  return product;
}

// Un componente que se carga mientras se hidrata o carga la aplicación
// es decir si lo que hagas en clientLoader tarda mucho,
// es el tiempo que este Loader se mostrará
export function HydrateFallback() {
  return <div>Loading...</div>;
}

// obtenemos la informacion igual como con el server loader
export default function Product({
  loaderData,
}: Route.ComponentProps) {
  const { name, description } = loaderData;
  return (
    <div>
      <h1>{name}</h1>
      <p>{description}</p>
    </div>
  );
}

Client Action

La función clientAction igualmente se ejecuta solo en el cliente y tiene prioridad; es decir, esta función se ejecuta antes que el server action y podría servir como una pre-validación antes de enviar la información al backend.

// routes/project.tsx
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";

export async function clientAction({
  request,
}: Route.ClientActionArgs) {
  let formData = await request.formData();
  let title = formData.get("title");
  return project;
}

export default function Project({
  actionData,
}: Route.ComponentProps) {
  return (
    <div>
      {actionData ? (
        <p>{actionData.title} updated</p>
      ) : null}
      <h1>Project</h1>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">Submit</button>
      </Form>
    </div>
  );
}

Conclusión

Veamos esto de una forma simplificada de forma ilustrada:

Action-loaders-functions-flow

Aquí se omiten ciertos detalles internos, como el route matching y route revalidation (shouldRevalidate) que de eso hablaré más adelante en otras publicaciones.

FunciónContextoEquivalente HTTP / PropósitoCuándo se ejecuta
loaderServidor (Node.js)GET (Carga de datos)Al entrar a la ruta, antes de enviar la UI inicial.
actionServidor (Node.js)POST, PUT, PATCH, DELETEAl enviar un formulario o mutar datos desde el cliente.
clientLoaderCliente (Navegador)GET (Pre-carga local)Después del server loader y antes de hidratar la aplicación.
clientActionCliente (Navegador)Mutación local previaAntes de que el server action procese los datos enviados.

Fuentes: https://reactrouter.com/start/framework/data-loading https://reactrouter.com/start/framework/actions