DEV Community

Cover image for React Router Data mode: Parte 3 - Loaders y carga de datos
Kevin Julián Martínez Escobar
Kevin Julián Martínez Escobar

Posted on • Edited on

React Router Data mode: Parte 3 - Loaders y carga de datos

Vamos con la tercera parte de esta serie de tutoriales. En este caso, veremos un concepto que viene de Remix y que ahora también podemos encontrar en React Router: los loaders.

Si vienes del tutorial anterior, puedes dejar el proyecto tal cual, pero si quieres asegurarte de que todo esté limpio o empezar desde el mismo punto, puedes ejecutar los siguientes comandos:

# Enlace del repositorio https://github.com/kevinccbsg/react-router-tutorial-devto
git reset --hard
git clean -d -f
git checkout 02-loaders-detail-page
Enter fullscreen mode Exit fullscreen mode

¿Qué son los loaders?

Los loaders son un mecanismo que React Router nos da para enviar información a nuestros componentes. Son funciones que añadimos en nuestra definición de rutas.

Este sería un ejemplo:

createBrowserRouter([
  {
    path: "/",
    loader: async () => {
      // return data from here
      return { records: await getSomeRecords() };
    },
    Component: MyRoute,
  },
]);
Enter fullscreen mode Exit fullscreen mode

En los loaders añadimos todo lo que consideramos necesario cargar en esa página. Al trabajar con React Router, debemos empezar a pensar por página, ya que toda la carga y mutación de datos se organiza a ese nivel.

En nuestra página de contactos, actualmente usamos un array con datos hardcodeados, lo cual no representa un caso real. Así que lo vamos a sustituir por una llamada a una API que hemos creado con json-server.
No entraremos en detalle sobre qué es json-server (es básicamente una forma rápida de simular una API REST con un archivo JSON), solo comentar que a partir de ahora probaremos la app con el comando:

npm run serve:dev
Enter fullscreen mode Exit fullscreen mode

Este comando nos levanta tanto la API con json-server como el frontend.

En la carpeta src/api están todos los métodos que llaman a la API usando Axios. Como esta serie trata de React Router, no entraremos en detalle sobre esa parte.

Una vez comentado el modo de trabajo, empezamos con el refactor. Normalmente en React, para hacer carga de datos en un componente, usamos algo como:

const [contacts, setContacts] = useState<Contact[]>([]);

useEffect(() => {
  fetchContacts()
    .then((data) => setContacts(data))
}, []);
Enter fullscreen mode Exit fullscreen mode

Con React Router, esto cambia, ya que usaremos un loader definido en AppRoutes.tsx:

import { createBrowserRouter } from "react-router";
import ContactsPage from "./pages/Contacts";
import ContactForm from "./pages/ContactForm";
import { fetchContacts } from "@/api/contacts";

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    // esta propiedad
    loader: async () => {
      const contacts = await fetchContacts();
      return { contacts };
    },
    Component: ContactsPage,
    children: [
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  {
    path: "/about",
    element: <div>About</div>,
  },
  {
    path: "*",
    element: <div>Not Found</div>,
  },
]);

export default AppRoutes;
Enter fullscreen mode Exit fullscreen mode

Aunque esto funciona, y luego podríamos acceder a los datos usando un hook de React Router, no te recomiendo hacerlo así. El archivo de rutas puede crecer mucho, y además más adelante necesitaremos resolver un problema de tipado con TypeScript, para lo cual este enfoque no es el ideal.

Lo mejor es crear un archivo separado, por ejemplo: src/pages/loader.tsx:

import { fetchContacts } from "@/api/contacts";

export const loadContacts = async () => {
  const contacts = await fetchContacts();
  return { contacts };
};
Enter fullscreen mode Exit fullscreen mode

Y así dejamos el archivo AppRoutes.tsx mucho más limpio:

import { createBrowserRouter } from "react-router";
import ContactsPage from "./pages/Contacts";
import ContactForm from "./pages/ContactForm";
import { loadContacts } from "./pages/loader";

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    loader: loadContacts,
    Component: ContactsPage,
    children: [
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  {
    path: "/about",
    element: <div>About</div>,
  },
  {
    path: "*",
    element: <div>Not Found</div>,
  },
]);

export default AppRoutes;
Enter fullscreen mode Exit fullscreen mode

Si navegamos a la web, veremos en la pestaña "Network" que se realiza la llamada a la API.

pestaña network con la llamada

Pero los datos siguen estando hardcodeados en la UI. ¿Cómo recuperamos la info?

Usamos el hook useLoaderData de React Router:

const { contacts } = useLoaderData();
Enter fullscreen mode Exit fullscreen mode

Este hook siempre nos devuelve lo que retorna el loader definido en la ruta del componente. En este caso, un objeto con contacts.

Ahora bien, si usamos esto en un proyecto con TypeScript, nos dará un error de tipado. Para solucionarlo, lo escribimos así:

const { contacts } = useLoaderData<typeof loadContacts>();
Enter fullscreen mode Exit fullscreen mode

Quedando el componente Contacts de la siguiente manera:

import { Link, Outlet, useLoaderData } from "react-router";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { loadContacts } from "./loader";

const ContactsPage = () => {
  const { contacts } = useLoaderData<typeof loadContacts>();
  return (
    <div className="h-screen grid grid-cols-[300px_1fr]">
      {/* Sidebar */}
      <div className="border-r p-4 flex flex-col gap-4">
        <Button className="w-full" variant="secondary" asChild>
          <Link to="/contacts/new" viewTransition>
            New
          </Link>
        </Button>
        <ScrollArea className="flex-1">
        <div className="flex flex-col gap-1 mt-4">
          {contacts.map(contact => (
            <Button
              key={contact.id}
              className="justify-start"
              asChild
            >
              <Link to={`/contacts/${contact.id}`} viewTransition>
                {contact.firstName} {contact.lastName}
              </Link>
            </Button>
          ))}
        </div>
      </ScrollArea>
      </div>
      {/* Detail View */}
      <div className="p-8">
        <Outlet />
      </div>
    </div>
  );
};

export default ContactsPage;
Enter fullscreen mode Exit fullscreen mode

Por eso es más cómodo tener un archivo de loader por página, ya que mejora la gestión de tipos y queda todo más separado.

Con esto, estamos haciendo lo mismo que hacíamos con useEffect, pero de la forma recomendada en React Router.


Por último, puede que hayas notado un warning en la terminal:

warning hidrate fallback

Esto se debe a que aún no tenemos una pantalla de carga. Podemos simular un delay en la API para ver el problema. En src/api/contacts.ts, descomenta la función delay y modifica fetchContacts:

export const fetchContacts = async () => {
  const response = await api.get<Contact[]>('/contacts');
  await delay(2000); // Simula latencia de red
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

Al recargar, veremos que la página queda en blanco unos segundos hasta que se cargan los datos. Esto es porque no hay un loading state. React Router permite manejar esto de varias formas, pero en este post usaremos la propiedad HydrateFallback.

En este proyecto ya tenemos el componente ContactsSkeletonPage, así que lo añadimos así:

import { createBrowserRouter } from "react-router";
import ContactsPage from "./pages/Contacts";
import ContactForm from "./pages/ContactForm";
import { loadContacts } from "./pages/loader";
import ContactsSkeletonPage from "./Layouts/HomeSkeleton";

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    loader: loadContacts,
    HydrateFallback: ContactsSkeletonPage,
    Component: ContactsPage,
    children: [
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  {
    path: "/about",
    element: <div>About</div>,
  },
  {
    path: "*",
    element: <div>Not Found</div>,
  },
]);

export default AppRoutes;
Enter fullscreen mode Exit fullscreen mode

Y con esto ya tenemos una pantalla de carga. Lo mejor es que esto se puede aplicar por página, y cada una carga de forma independiente sin bloquear a las demás.

Esto lo veremos en más detalle en el siguiente post sobre loaders.
Sin duda, es una de las partes más importantes y potentes de React Router, y lo será aún más cuando lo combinemos con las actions.

¡Nos vemos en la parte 4! (Próximamente)

Top comments (0)