React Router Data Mode: Parte 4 – Rutas con parámetros, useRouteLoaderData y useParams
Kevin Julián Martínez Escobar

Kevin Julián Martínez Escobar @kevinccbsg

Location:
Spain
Joined:
Dec 31, 2019

React Router Data Mode: Parte 4 – Rutas con parámetros, useRouteLoaderData y useParams

Publish Date: Jun 4
0 0

Continuamos con la cuarta entrega de esta serie sobre React Router data mode. En esta ocasión seguimos profundizando en los loaders, primero añadiendo la pantalla de detalle de un contacto y luego explorando hooks como useRouteLoaderData y useParams.


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 03-loaders-detail-page
Enter fullscreen mode Exit fullscreen mode

Un poco de repaso

Hasta ahora solo tenemos las rutas / y contacts/new, pero nos falta algo clave: una página de detalle para cada contacto. Esta página vivirá dentro de la ruta raíz /, así que debemos:

  1. Crear la nueva página.
  2. Añadir una ruta anidada con parámetro.
  3. Revisar nuestros enlaces.

1. Creamos la página

Creamos src/pages/ContactDetail.tsx, por ahora con un diseño muy básico. En la próxima parte mejoraremos el UI:


const contact = {
  firstName: 'John',
  username: 'john_doe',
};

const ContactDetail = () => {
  return (
    <div>
      <h2>Contact Detail</h2>
      <p>{contact.firstName}</p>
      <p>{contact.username}</p>
    </div>
  );
}

export default ContactDetail;
Enter fullscreen mode Exit fullscreen mode

2. Creamos la ruta anidada

Queremos que la ruta cambie según el contacto: contacts/:contactId. No usaremos query params como ?id=123, porque no estamos filtrando una lista, sino accediendo a un recurso individual.

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    loader: loadContacts,
    HydrateFallback: ContactsSkeletonPage,
    Component: ContactsPage,
    children: [
      {
        path: "contacts/:contactId",
        Component: ContactDetail,
      },
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  ... // the other routes
]);
Enter fullscreen mode Exit fullscreen mode

3. Revisar nuestros enlaces

Nuestros enlaces ya estaban bien definidos:

<Link to={`/contacts/${contact.id}`} viewTransition>
  {contact.firstName} {contact.lastName}
</Link>
Enter fullscreen mode Exit fullscreen mode

Sin embargo, ahora podemos ver que al seleccionar cada contacto la URL cambia pero el contenido es el mismo. Esto es porque nuestra página siempre muestra el mismo contenido. Tenemos que añadir nuestro loader.


Añadimos el loader para el detalle

Ahora mismo, da igual qué contacto seleccionemos: siempre se muestra el mismo contenido. Necesitamos un loader para que cargue el contacto correcto.

Creamos un nuevo método en src/pages/loader.ts. Esta vez sí necesitaremos acceder a params para obtener el contactId.

export const loadContactDetail = async ({ params }: LoaderFunctionArgs) => {
  const contactId = params.contactId;
  /*
  Aquí validamos que contactId exista.
  La gestión de errores 404 o respuestas inválidas las veremos en
  otro post
  */
  if (!contactId) {
    throw new Error("Contact ID is required");
  }
  const contact = await fetchContactById(contactId);
  return { contact };
};
Enter fullscreen mode Exit fullscreen mode

También actualizamos la función en src/api/contacts.ts para simular un delay:

export const fetchContactById = async (id: string) => {
  const response = await api.get<Contact>(`/contacts/${id}`);
  await delay(500); // Simulate network delay
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

Y ahora sí, conectamos todo en las rutas:

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    loader: loadContacts,
    HydrateFallback: ContactsSkeletonPage,
    Component: ContactsPage,
    children: [
      {
        path: "contacts/:contactId",
        loader: loadContactDetail,
        Component: ContactDetail,
      },
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  ...
]);
Enter fullscreen mode Exit fullscreen mode

Y en el componente:

import { useLoaderData } from "react-router";
import { loadContactDetail } from "./loader";

const ContactDetail = () => {
  const { contact } = useLoaderData<typeof loadContactDetail>();
  return (
    <div>
      <h2>Contact Detail</h2>
      <p>{contact.firstName}</p>
      <p>{contact.username}</p>
    </div>
  );
}

export default ContactDetail;
Enter fullscreen mode Exit fullscreen mode

Con esto ya tenemos nuestra pantalla de detalle funcional usando loaders. 🎯


¿Pero no teníamos ya ese dato en otro loader?

Nuestra pantalla de detalle vive dentro de la ruta principal /, la cual ya carga todos los contactos. ¿Tiene sentido hacer otra llamada a la API solo para mostrar un contacto que ya tenemos?

Podemos evitarlo utilizando el hook useRouteLoaderData.

Usando useRouteLoaderData

Este hook permite acceder a los datos de un loader. En nuestro caso, la ruta raíz (/).

Primero, le damos un id a esa ruta y eliminamos el loader del detalle:

const AppRoutes = createBrowserRouter([
  {
    path: "/",
    loader: loadContacts,
    id: "root",
    HydrateFallback: ContactsSkeletonPage,
    Component: ContactsPage,
    children: [
      {
        path: "contacts/:contactId",
        Component: ContactDetail,
      },
      {
        path: "contacts/new",
        Component: ContactForm,
      },
    ],
  },
  ...
]);
Enter fullscreen mode Exit fullscreen mode

Y en el componente:

import { useRouteLoaderData } from "react-router";
import { loadContacts } from "./loader";

const ContactDetail = () => {
  const routeData = useRouteLoaderData<typeof loadContacts>("root");
  if (!routeData) {
    return <div>Loading...</div>;
  }
  const contact = routeData.contacts[0]; // Por simplicidad, mostramos el primero
  return (
    <div>
      <h2>Contact Detail</h2>
      <p>{contact.firstName}</p>
      <p>{contact.username}</p>
    </div>
  );
}

export default ContactDetail;
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, usamos useRouteLoaderData con el ID "root" para acceder a los datos ya cargados por el loader de la ruta principal. Además, añadimos typeof loadContacts para que TypeScript nos dé autocompletado y chequeo de tipos. Es importante validar que los datos existan, ya que en la primera carga podrían no estar disponibles. Por eso mostramos un fallback mientras tanto (Loading...).

Pero necesitamos saber qué contacto mostrar

Para eso usamos useParams para acceder a los params que tengamos definidos en la URL:

import { useParams, useRouteLoaderData } from "react-router";
import { loadContacts } from "./loader";

const ContactDetail = () => {
  const { contactId } = useParams<{ contactId: string }>(); // Needs TS type annotation
  const routeData = useRouteLoaderData<typeof loadContacts>("root");
  if (!routeData) {
    return <div>Loading...</div>;
  }

  const { contacts } = routeData;

  // Find the contact locally
  const contact = contacts.find(({ id }) => id === contactId);

  if (!contact) {
    return <div>Contact not found</div>;
  }
  return (
    <div>
      <h2>Contact Detail</h2>
      <p>{contact.firstName}</p>
      <p>{contact.username}</p>
    </div>
  );
}

export default ContactDetail;
Enter fullscreen mode Exit fullscreen mode

Tenemos la navegación entre contactos funcionando con los datos ya cargados, sin peticiones extra. También repasamos cómo usar los params en los loaders y en los componentes, con todo bien tipado.


En la parte 5 mejoraremos el diseño del detalle y haremos refactor en la navegación, asegurándonos de que el enlace activo se marque correctamente.
¡Nos vemos en la próxima!

Comments 0 total

    Add comment