react-router-tutorial-p1

React Router Fundamentals - Tutorial Completo y Detallado

Introducción

¿Qué es React Router?

React Router es una biblioteca fundamental en el ecosistema de React, diseñada para manejar el enrutamiento en aplicaciones de una sola página (Single Page Applications, o SPAs). En el contexto del desarrollo web moderno, las SPAs han ganado popularidad por su capacidad para ofrecer una experiencia de usuario fluida y dinámica, similar a una aplicación de escritorio.

¿Qué Problema Resuelve React Router?

El Problema Tradicional: Tradicionalmente, las páginas web recargan todo el contenido cada vez que el usuario navega a una sección diferente (haciendo clic en un link). Este proceso:

La Solución de las SPAs: Las Single Page Applications resuelven este problema ofreciendo una experiencia más fluida, donde el contenido se carga dinámicamente sin necesidad de refrescar toda la página. Esto se logra mediante JavaScript, que gestiona la carga de contenido y la actualización de la interfaz de usuario de manera selectiva.

El Desafío Específico que Resuelve React Router: Dentro de las SPAs, surge un nuevo desafío: cómo gestionar la navegación y el mantenimiento del historial del navegador de manera que la aplicación se comporte como una página web tradicional en términos de navegación.

Sin React Router, una SPA tendría estos problemas:

Beneficios de React Router:


Configuración Inicial del Proyecto

Requisitos Previos

Antes de comenzar, necesitas tener instalado Node.js en tu sistema, ya que utilizaremos npm (Node Package Manager) para gestionar las dependencias del proyecto.

Paso 1: Crear el Proyecto con Vite

Vite es un bundler (empaquetador) y servidor de desarrollo moderno que ofrece:

Comando para crear el proyecto:

npm create vite@latest name-of-your-project -- --template react

Este comando:

  1. Descarga la última versión de Vite
  2. Crea un nuevo proyecto con plantilla de React
  3. Configura la estructura básica de carpetas

Seguir con la configuración:

cd <your new project directory>
npm install react-router-dom # Biblioteca principal de React Router
npm install localforage match-sorter sort-by # Dependencias solo para el tutorial
npm run dev # Inicia el servidor de desarrollo

Explicación de las dependencias del tutorial:

Paso 2: Configurar los Archivos Base

Estructura de carpetas resultante:

src/
├── contacts.js     # Módulo de datos (simula una API)
├── index.css       # Estilos globales pre-escritos
└── main.jsx        # Punto de entrada de la aplicación

Archivos a eliminar:

¿Por qué esta estructura? Mantenemos el proyecto simple y enfocado en React Router, eliminando elementos que no son necesarios para aprender los conceptos fundamentales de enrutamiento.


1. Creando el Primer Router

Concepto: Browser Router

El Browser Router es el componente principal que habilita el enrutamiento del lado del cliente. Utiliza la API de History del navegador para mantener la UI sincronizada con la URL.

Implementación en main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import "./index.css";

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Análisis línea por línea:

  1. Importaciones:
    • React y ReactDOM: Bibliotecas fundamentales de React
    • createBrowserRouter: Función para crear la configuración de rutas
    • RouterProvider: Componente que provee el router a toda la aplicación
  2. Configuración del router:
    const router = createBrowserRouter([...])
    
    • Recibe un array de objetos de configuración de rutas
    • Cada objeto define una ruta con su path y element
  3. Primera ruta (Root Route):
    {
      path: "/",
      element: <div>Hello world!</div>,
    }
    
    • path: "/": Define que esta ruta responde a la URL raíz
    • element: El componente React que se renderizará en esta ruta
  4. Renderizado:
    <RouterProvider router={router} />
    
    • Inyecta el router en toda la aplicación
    • Permite que los componentes hijos accedan al contexto de enrutamiento

Concepto: Root Route (Ruta Raíz) La ruta raíz es especial porque:


2. Creando el Componente Root Layout

¿Qué es un Layout Component?

Un Layout Component es un componente que:

Creando la Estructura de Carpetas

mkdir src/routes
touch src/routes/root.jsx

Esta organización:

Implementación del Componente Root

export default function Root() {
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          {/* Formulario de búsqueda */}
          <form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
            />
            <div id="search-spinner" aria-hidden hidden={true} />
            <div className="sr-only" aria-live="polite"></div>
          </form>
          
          {/* Botón para crear nuevo contacto */}
          <form method="post">
            <button type="submit">New</button>
          </form>
        </div>
        
        {/* Navegación con lista de contactos */}
        <nav>
          <ul>
            <li>
              <a href={`/contacts/1`}>Your Name</a>
            </li>
            <li>
              <a href={`/contacts/2`}>Your Friend</a>
            </li>
          </ul>
        </nav>
      </div>
      
      {/* Área de detalle donde se mostrarán las rutas hijas */}
      <div id="detail"></div>
    </>
  );
}

Análisis de elementos clave:

  1. Fragment (<>...</>):
    • Permite retornar múltiples elementos sin agregar nodos DOM extras
    • Mantiene el HTML limpio
  2. Sidebar:
    • Contiene navegación y controles
    • Será visible en todas las páginas (layout persistente)
  3. Search Form:
    • role="search": Atributo de accesibilidad
    • aria-label: Descripción para lectores de pantalla
    • Mejora la experiencia de usuarios con discapacidades
  4. Formulario de creación:
    • method="post": Indica que enviará datos (no solo navegación)
    • Por ahora usa HTML nativo (lo mejoraremos después)
  5. Área de detalle:
    • Espacio reservado para contenido dinámico
    • Aquí se renderizarán las rutas hijas

Conectando Root al Router

import Root from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);

Resultado visual: Tu aplicación ahora muestra el layout completo con sidebar y área de detalle, aunque los links aún no funcionan correctamente con enrutamiento del lado del cliente.


3. Manejo de Errores (Error Boundaries)

¿Por Qué Es Importante el Manejo de Errores?

El manejo proactivo de errores:

El Problema Actual

Si haces clic en los links del sidebar (/contacts/1), verás:

¿Por qué ocurre este error? La ruta /contacts/1 no está definida en nuestra configuración de router, por lo que React Router lanza un error 404.

Creando un Componente de Error Personalizado

// src/error-page.jsx
import { useRouteError } from "react-router-dom";

export default function ErrorPage() {
  const error = useRouteError();
  console.error(error);

  return (
    <div id="error-page">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>
        <i>{error.statusText || error.message}</i>
      </p>
    </div>
  );
}

Análisis del código:

  1. useRouteError() Hook:
    • Hook personalizado de React Router
    • Retorna el objeto de error que fue lanzado
    • Contiene información como statusText, message, status
  2. console.error(error):
    • Registra el error en la consola del navegador
    • Útil para debugging durante el desarrollo
    • No afecta la visualización para el usuario
  3. Visualización del error:
    {error.statusText || error.message}
    
    • statusText: Texto descriptivo del error HTTP (ej: “Not Found”)
    • message: Mensaje de error de JavaScript
    • Operador ||: Muestra el primero que esté disponible

Configurando el Error Element

import ErrorPage from "./error-page";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
]);

Cómo funciona errorElement:

Tipos de errores comunes que captura:


4. Creando la Ruta de Contacto

Concepto: Rutas Múltiples

Una aplicación real necesita múltiples rutas para diferentes vistas. En nuestro caso:

Creando el Componente Contact

// src/routes/contact.jsx
import { Form } from "react-router-dom";

export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://robohash.org/you.png?size=200x200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    <div id="contact">
      <div>
        <img
          key={contact.avatar}
          src={
            contact.avatar ||
            `https://robohash.org/${contact.id}.png?size=200x200`
          }
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a
              target="_blank"
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        )}

        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={(event) => {
              if (
                !confirm(
                  "Please confirm you want to delete this record."
                )
              ) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  const favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

Análisis detallado:

  1. Datos temporales (hardcoded):
    const contact = { ... }
    
    • Por ahora usamos datos estáticos
    • Más adelante cargaremos datos reales con loaders
  2. Renderizado condicional de avatar:
    contact.avatar || `https://robohash.org/${contact.id}.png?size=200x200`
    
    • Si existe avatar personalizado, lo usa
    • Si no, genera uno automático con Robohash
    • Robohash crea avatars únicos basados en el ID
  3. Renderizado condicional del nombre:
    {contact.first || contact.last ? (...) : (<i>No Name</i>)}
    
    • Verifica si existe nombre o apellido
    • Si no existe ninguno, muestra “No Name”
    • Mejora la UX mostrando información clara
  4. Componente <Form> de React Router:
    • Similar a <form> HTML pero con características adicionales
    • Intercepta el submit para usar client-side routing
    • action="edit": Define una acción relativa a la ruta actual
  5. Confirmación de eliminación:
    onSubmit={(event) => {
      if (!confirm("Please confirm...")) {
        event.preventDefault();
      }
    }}
    
    • Muestra diálogo nativo de confirmación
    • event.preventDefault(): Cancela el envío si el usuario cancela
    • Buena práctica para acciones destructivas
  6. Componente Favorite:
    • Componente separado para botón de favoritos
    • Alterna entre estrella llena (★) y vacía (☆)
    • Usa aria-label para accesibilidad

Agregando la Ruta al Router

import Contact from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
]);

Análisis de la nueva ruta:

  1. Path con parámetro dinámico:
    path: "contacts/:contactId"
    
    • :contactId: Segmento dinámico (puede ser cualquier valor)
    • Coincide con URLs como: /contacts/1, /contacts/abc, /contacts/123
    • El valor se captura y está disponible en el componente
  2. Problema actual:
    • La ruta funciona pero se renderiza en toda la página
    • No aparece dentro del layout de Root
    • Necesitamos usar rutas anidadas para solucionarlo

5. Rutas Anidadas (Nested Routes)

Concepto: Rutas Anidadas

Las rutas anidadas permiten:

Visualización conceptual:

Root Layout (siempre visible)
  ├── Sidebar
  ├── Header
  └── <Outlet> ← Aquí se renderizan las rutas hijas
       ├── Home
       ├── Contact Details
       └── Edit Contact

Configurando Rutas Anidadas

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [  // ← Rutas hijas
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

Cambios clave:

Jerarquía de paths resultante:

Usando el Componente <Outlet>

El <Outlet> es el punto donde se renderizarán las rutas hijas.

import { Outlet } from "react-router-dom";

export default function Root() {
  return (
    <>
      <div id="sidebar">
        {/* Contenido del sidebar */}
      </div>
      <div id="detail">
        <Outlet />  {/* ← Las rutas hijas se renderizan aquí */}
      </div>
    </>
  );
}

Cómo funciona <Outlet>:

  1. Contexto de renderizado:
    • React Router determina qué ruta hija debe mostrarse
    • Busca en el array children de la ruta actual
    • Renderiza el element correspondiente en la posición del <Outlet>
  2. Ejemplo de flujo:
    • Usuario visita /contacts/1
    • React Router renderiza <Root>
    • Dentro de Root, <Outlet> se reemplaza por <Contact>
    • Resultado: Sidebar + Contact details
  3. Ventajas:
    • El sidebar permanece en pantalla (no se re-renderiza)
    • Solo el contenido del área de detalle cambia
    • Transiciones suaves entre vistas
    • Mejor performance

Concepto importante: Composición vs Inclusión


6. Client Side Routing (Enrutamiento del Lado del Cliente)

Actualmente, los links en el sidebar usan <a href>:

<a href={`/contacts/1`}>Your Name</a>

¿Qué ocurre al hacer clic?

  1. El navegador hace una petición HTTP completa al servidor
  2. Se descarga todo el HTML, CSS y JavaScript nuevamente
  3. La página se recarga completamente
  4. Se pierde el estado de la aplicación
  5. Experiencia lenta y poco fluida

Puedes verificarlo:

React Router proporciona el componente <Link> que:

Implementando Client Side Routing

import { Outlet, Link } from "react-router-dom";

export default function Root() {
  return (
    <>
      <div id="sidebar">
        <nav>
          <ul>
            <li>
              <Link to={`contacts/1`}>Your Name</Link>
            </li>
            <li>
              <Link to={`contacts/2`}>Your Friend</Link>
            </li>
          </ul>
        </nav>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

Diferencias clave entre <a> y <Link>:

Característica <a href> <Link to>
Recarga de página Sí No
Petición al servidor Completa Solo datos si es necesario
Mantiene estado No Sí
Velocidad Lenta Instantánea
History API Básica Avanzada
Previene default Manual Automático

¿Cómo funciona internamente <Link>?

  1. Intercepta el click:
    event.preventDefault() // Previene navegación default
    
  2. Actualiza la URL:
    window.history.pushState({}, '', newURL)
    
  3. Notifica a React Router:
    • React Router detecta el cambio de URL
    • Determina qué componente debe renderizarse
    • Actualiza el <Outlet> con el nuevo componente
  4. Resultado:
    • URL actualizada en la barra del navegador
    • Nuevo contenido renderizado
    • Sin recarga de página
    • Botones adelante/atrás funcionan correctamente

Verificación en DevTools:


7. Cargando Datos con Loaders

Concepto: Acoplamiento Natural entre URLs, Layouts y Datos

En la mayoría de aplicaciones web, existe una relación natural:

Segmento de URL Componente Datos Necesarios
/ <Root> Lista de todos los contactos
/contacts/:id <Contact> Detalles del contacto específico

React Router aprovecha este acoplamiento natural con el concepto de loaders.

¿Qué es un Loader?

Un loader es:

Ventajas sobre fetch tradicional en useEffect:

❌ Enfoque tradicional (useEffect):

function Component() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []);
  
  if (loading) return <Spinner />;
  return <div>{data}</div>;
}

Problemas:

✅ Enfoque con loaders:

export async function loader() {
  const data = await fetch('/api/data');
  return data;
}

function Component() {
  const data = useLoaderData();
  return <div>{data}</div>; // Datos siempre disponibles
}

Ventajas:

Implementando el Loader en Root

Paso 1: Crear y exportar el loader

// src/routes/root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "../contacts";

export async function loader() {
  const contacts = await getContacts();
  return { contacts };
}

export default function Root() {
  // ... código del componente
}

Análisis del loader:

  1. Función asíncrona:
    export async function loader()
    
    • Debe ser asíncrona para esperar datos
    • Se exporta para usarla en la configuración del router
  2. Llamada a la API/base de datos:
    const contacts = await getContacts();
    
    • getContacts() simula una llamada a API
    • En producción, sería una petición fetch real
    • await espera a que se resuelva la promesa
  3. Retorno de datos:
    return { contacts };
    
    • Retorna un objeto con los datos
    • Puede retornar cualquier valor serializable
    • También puede retornar una Response de la Web API

Paso 2: Configurar el loader en la ruta

import Root, { loader as rootLoader } from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,  // ← Loader configurado
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

Nomenclatura importante:

import Root, { loader as rootLoader }

Paso 3: Acceder a los datos con useLoaderData

import {
  Outlet,
  Link,
  useLoaderData,
} from "react-router-dom";

export default function Root() {
  const { contacts } = useLoaderData();
  
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        
        <nav>
          {contacts.length ? (
            <ul>
              {contacts.map((contact) => (
                <li key={contact.id}>
                  <Link to={`contacts/${contact.id}`}>
                    {contact.first || contact.last ? (
                      <>
                        {contact.first} {contact.last}
                      </>
                    ) : (
                      <i>No Name</i>
                    )}{" "}
                    {contact.favorite && <span>★</span>}
                  </Link>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

Análisis del renderizado:

  1. Hook useLoaderData:
    const { contacts } = useLoaderData();
    
    • Accede a los datos retornados por el loader
    • Tipado seguro (TypeScript conoce la estructura)
    • Datos siempre disponibles (no null/undefined)
  2. Renderizado condicional:
    {contacts.length ? (...) : (...)}
    
    • Verifica si hay contactos
    • Muestra lista o mensaje “No contacts”
    • Buena experiencia de usuario
  3. Mapeo de contactos:
    {contacts.map((contact) => (...))}
    
    • Crea un <li> por cada contacto
    • key={contact.id}: Clave única requerida por React
    • Link dinámico a cada contacto
  4. Indicador de favorito:
    {contact.favorite && <span>★</span>}
    
    • Renderizado condicional con &&
    • Solo muestra estrella si es favorito

Sincronización Automática

Aspecto revolucionario:

// NO necesitas hacer esto manualmente:
const [contacts, setContacts] = useState([]);
useEffect(() => {
  fetch('/api/contacts')
    .then(res => res.json())
    .then(data => setContacts(data));
}, []);

React Router mantiene automáticamente los datos sincronizados con la UI:


8. Escribiendo Datos con HTML Forms y Actions

Concepto: Primitiva de Mutación de Datos

React Router emula la navegación con formularios HTML como primitiva para mutación de datos, siguiendo el modelo web tradicional pero con las ventajas del renderizado del lado del cliente.

¿Cómo Funcionan los Formularios HTML Tradicionales?

Comportamiento nativo del navegador:

  1. Formulario GET:
    <form method="get" action="/search">
      <input name="q" value="react" />
      <button type="submit">Search</button>
    </form>
    
    • Al enviar: navegación a /search?q=react
    • Datos en la URL (query params)
    • Recarga la página
  2. Formulario POST:
    <form method="post" action="/contacts">
      <input name="name" value="John" />
      <button type="submit">Create</button>
    </form>
    
    • Al enviar: petición POST a /contacts
    • Datos en el cuerpo de la petición (request body)
    • El servidor procesa y responde
    • Recarga la página con la respuesta

La innovación de React Router:

Creando Contactos con Actions

Paso 1: Exportar una action en root.jsx

import {
  Outlet,
  Link,
  useLoaderData,
  Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";

export async function action() {
  const contact = await createContact();
  return { contact };
}

export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form method="post">
            <button type="submit">New</button>
          </Form>
        </div>
        {/* resto del código */}
      </div>
    </>
  );
}

Cambios importantes:

  1. Importación de <Form>:
    import { Form } from "react-router-dom";
    
    • Componente especial de React Router
    • Similar a <form> HTML pero con client-side routing
  2. La función action:
    export async function action() {
      const contact = await createContact();
      return { contact };
    }
    
    • Función asíncrona que procesa mutaciones
    • createContact() crea un contacto vacío
    • Retorna el contacto creado
  3. Formulario con <Form>:
    <Form method="post">
      <button type="submit">New</button>
    </Form>
    
    • method="post": Indica mutación de datos
    • Sin action prop: envía a la ruta actual
    • React Router intercepta el submit

Paso 2: Configurar la action en la ruta

import Root, {
  loader as rootLoader,
  action as rootAction,
} from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,  // ← Action configurada
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

El Flujo Completo de una Mutación

Cuando haces clic en “New”:

  1. Interceptación del formulario:
    • <Form> detecta el submit
    • event.preventDefault() previene envío tradicional
    • Serializa los datos del formulario
  2. Llamada a la action:
    • React Router identifica la ruta actual (/)
    • Ejecuta la action configurada (rootAction)
    • Espera a que se complete
  3. Revalidación automática:
    • Una vez la action finaliza, React Router revalida datos
    • Re-ejecuta todos los loaders de las rutas activas
    • En este caso, re-ejecuta rootLoader
  4. Actualización de UI:
    • useLoaderData() recibe los datos actualizados
    • El componente se re-renderiza automáticamente
    • El nuevo contacto aparece en la lista

La magia del modelo:

// NO necesitas escribir código como este:
const [contacts, setContacts] = useState([]);

async function handleCreateContact() {
  const newContact = await createContact();
  setContacts([...contacts, newContact]); // Actualización manual
}

React Router lo hace automáticamente:

Concepto: Separación de Responsabilidades

Loaders vs Actions:

Aspecto Loaders Actions
Propósito Leer datos Escribir/modificar datos
Cuándo se ejecutan Al navegar a la ruta Al enviar formularios
Método HTTP equivalente GET POST, PUT, DELETE, PATCH
Efecto secundario Revalida datos después de actions Ninguno

9. Parámetros de URL en Loaders

Concepto: Segmentos Dinámicos de URL

Actualmente, al hacer clic en un contacto, vemos datos hardcoded. Necesitamos cargar datos reales basados en el ID de la URL.

Revisando la configuración de ruta:

{
  path: "contacts/:contactId",
  element: <Contact />,
}

El segmento :contactId:

Accediendo a URL Params en Loaders

Los parámetros se pasan automáticamente al loader a través del objeto params.

Paso 1: Crear el loader en contact.jsx

// src/routes/contact.jsx
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";

export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  return { contact };
}

export default function Contact() {
  const { contact } = useLoaderData();
  
  return (
    <div id="contact">
      <div>
        <img
          key={contact.avatar}
          src={
            contact.avatar ||
            `https://robohash.org/${contact.id}.png?size=200x200`
          }
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a
              target="_blank"
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        )}

        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={(event) => {
              if (
                !confirm(
                  "Please confirm you want to delete this record."
                )
              ) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  const favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

Análisis del loader:

  1. Desestructuración de params:
    export async function loader({ params }) {
    
    • El loader recibe un objeto con varias propiedades
    • params contiene los parámetros de URL
    • Desestructuramos directamente en la firma
  2. Acceso al parámetro específico:
    const contact = await getContact(params.contactId);
    
    • params.contactId coincide con el nombre :contactId en la ruta
    • Si la ruta fuera users/:userId, sería params.userId
    • Los nombres deben coincidir exactamente
  3. Flujo completo:
    • Usuario hace clic en /contacts/123
    • React Router ejecuta el loader
    • params.contactId = “123”
    • getContact("123") busca el contacto
    • Retorna los datos del contacto
    • Componente renderiza con datos reales

Paso 2: Configurar el loader en la ruta

import Contact, {
  loader as contactLoader,
} from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,  // ← Loader configurado
      },
    ],
  },
]);

Otros Parámetros Disponibles en Loaders

Además de params, los loaders reciben:

export async function loader({ params, request }) {
  // params: { contactId: "123" }
  // request: objeto Request de la Web API
  
  const url = new URL(request.url);
  const searchQuery = url.searchParams.get('q');
  
  // Ejemplo: /contacts/123?q=john
  // params.contactId = "123"
  // searchQuery = "john"
}

Propiedades útiles del objeto request:


10. Actualizando Datos con Formularios

Concepto: Formularios de Edición

Ahora crearemos una ruta para editar contactos existentes: /contacts/:contactId/edit

Creando la Ruta de Edición

Paso 1: Crear el componente edit.jsx

// src/routes/edit.jsx
import { Form, useLoaderData } from "react-router-dom";

export default function EditContact() {
  const { contact } = useLoaderData();

  return (
    <Form method="post" id="contact-form">
      <p>
        <span>Name</span>
        <input
          placeholder="First"
          aria-label="First name"
          type="text"
          name="first"
          defaultValue={contact?.first}
        />
        <input
          placeholder="Last"
          aria-label="Last name"
          type="text"
          name="last"
          defaultValue={contact?.last}
        />
      </p>
      <label>
        <span>Twitter</span>
        <input
          type="text"
          name="twitter"
          placeholder="@jack"
          defaultValue={contact?.twitter}
        />
      </label>
      <label>
        <span>Avatar URL</span>
        <input
          placeholder="https://example.com/avatar.jpg"
          aria-label="Avatar URL"
          type="text"
          name="avatar"
          defaultValue={contact?.avatar}
        />
      </label>
      <label>
        <span>Notes</span>
        <textarea
          name="notes"
          defaultValue={contact?.notes}
          rows={6}
        />
      </label>
      <p>
        <button type="submit">Save</button>
        <button type="button">Cancel</button>
      </p>
    </Form>
  );
}

Análisis de características importantes:

  1. defaultValue vs value:
    <input
      name="first"
      defaultValue={contact?.first}
    />
    
    • defaultValue: Valor inicial, pero el input puede cambiar
    • value: Input controlado (requiere onChange)
    • Para formularios HTML tradicionales, usa defaultValue
  2. Optional chaining (?.):
    defaultValue={contact?.first}
    
    • Previene errores si contact es null/undefined
    • Equivalente a: contact && contact.first
    • Retorna undefined si contact no existe
  3. Atributo name en inputs:
    <input name="first" />
    <input name="last" />
    <input name="twitter" />
    
    • Crucial para FormData
    • Define la clave en el objeto de datos
    • Debe coincidir con la estructura de datos esperada

Paso 2: Agregar la ruta al router

import EditContact from "./routes/edit";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
      },
      {
        path: "contacts/:contactId/edit",
        element: <EditContact />,
        loader: contactLoader,  // ← Reutiliza el mismo loader
      },
    ],
  },
]);

Nota sobre reutilización de loaders:

Implementando la Action de Actualización

Paso 1: Crear la action en edit.jsx

import {
  Form,
  useLoaderData,
  redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";

export async function action({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}

export default function EditContact() {
  // ... código del componente
}

Análisis detallado de la action:

  1. Obtención de FormData:
    const formData = await request.formData();
    
    • request: objeto Request de la Web API
    • formData(): método que retorna una Promise
    • FormData contiene todos los campos del formulario
  2. ¿Qué es FormData?
    • API nativa del navegador
    • Representa datos de un formulario
    • Similar a un Map de JavaScript
    • Métodos principales:
      formData.get("first")     // Obtiene un valor
      formData.getAll("tags")   // Obtiene array de valores
      formData.has("email")     // Verifica existencia
      formData.entries()        // Itera sobre entradas
      
  3. Conversión a objeto:
    const updates = Object.fromEntries(formData);
    
    • Object.fromEntries(): Convierte iterables a objeto
    • Ejemplo de transformación:
      // FormData:
      // first: "John"
      // last: "Doe"
      // twitter: "@johndoe"
           
      // Resultado:
      {
        first: "John",
        last: "Doe",
        twitter: "@johndoe"
      }
      
  4. Actualización de datos:
    await updateContact(params.contactId, updates);
    
    • params.contactId: ID del contacto desde la URL
    • updates: Objeto con los campos actualizados
    • En producción, sería una petición PUT/PATCH a la API
  5. Redirección:
    return redirect(`/contacts/${params.contactId}`);
    
    • Helper de React Router para redireccionar
    • Retorna una Response con código 302
    • Causa navegación a la nueva URL

Paso 2: Configurar la action

import EditContact, {
  action as editAction,
} from "./routes/edit";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
      },
      {
        path: "contacts/:contactId/edit",
        element: <EditContact />,
        loader: contactLoader,
        action: editAction,  // ← Action configurada
      },
    ],
  },
]);

Flujo Completo de Actualización

Cuando el usuario guarda el formulario:

  1. Submit del formulario:
    • Usuario llena campos y hace clic en “Save”
    • <Form method="post"> intercepta el submit
    • Serializa todos los inputs con atributo name
  2. Ejecución de la action:
    • React Router llama a editAction
    • request.formData() obtiene los datos
    • Object.fromEntries() los convierte a objeto
    • updateContact() actualiza la base de datos/storage
  3. Redirección:
    • redirect() retorna Response de redirección
    • React Router navega a /contacts/:contactId
  4. Revalidación automática:
    • React Router detecta la mutación
    • Re-ejecuta loaders de rutas activas
    • rootLoader: Actualiza lista de contactos
    • contactLoader: Actualiza detalles del contacto
  5. Actualización de UI:
    • Ambos componentes reciben datos frescos
    • Se re-renderizan automáticamente
    • El usuario ve los cambios inmediatamente

Concepto: El Modelo Web Tradicional Mejorado

Web Tradicional (sin JavaScript):

1. Usuario envía formulario
2. Servidor procesa
3. Servidor responde con HTML completo
4. Página se recarga completamente
5. Se pierde scroll position y estado

React Router (con client-side routing):

1. Usuario envía formulario
2. Action procesa (cliente o servidor)
3. Revalidación de datos
4. Actualización selectiva de UI
5. Se mantiene scroll position y estado

Ventajas del enfoque de React Router:


11. Mejorando la Experiencia: Redirección Automática

Problema Actual

Cuando creamos un nuevo contacto:

Sería mejor redirigir automáticamente al formulario de edición del nuevo contacto.

Actualizando la Action de Creación

// src/routes/root.jsx
import {
  Outlet,
  Link,
  useLoaderData,
  Form,
  redirect,  // ← Importar redirect
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";

export async function action() {
  const contact = await createContact();
  return redirect(`/contacts/${contact.id}/edit`);  // ← Redirección
}

Cambios:

  1. Importamos redirect de React Router
  2. Después de crear el contacto, redirigimos
  3. URL destino: /contacts/[nuevo-id]/edit

Resultado:

Concepto: UX fluida Este patrón es común en aplicaciones CRUD:


Problema de UX

Con múltiples contactos en la lista, no es obvio cuál estamos viendo actualmente.

React Router proporciona <NavLink>, una versión mejorada de <Link> con capacidades de styling activo.

// src/routes/root.jsx
import {
  Outlet,
  NavLink,  // ← Cambiar de Link a NavLink
  useLoaderData,
  Form,
  redirect,
} from "react-router-dom";

export default function Root() {
  const { contacts } = useLoaderData();
  
  return (
    <>
      <div id="sidebar">
        <nav>
          {contacts.length ? (
            <ul>
              {contacts.map((contact) => (
                <li key={contact.id}>
                  <NavLink
                    to={`contacts/${contact.id}`}
                    className={({ isActive, isPending }) =>
                      isActive
                        ? "active"
                        : isPending
                        ? "pending"
                        : ""
                    }
                  >
                    {contact.first || contact.last ? (
                      <>
                        {contact.first} {contact.last}
                      </>
                    ) : (
                      <i>No Name</i>
                    )}{" "}
                    {contact.favorite && <span>★</span>}
                  </NavLink>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

Análisis del NavLink:

  1. Prop className como función:
    className={({ isActive, isPending }) => ...}
    
    • Recibe objeto con estados del link
    • Retorna string con clase(s) CSS
  2. Estados disponibles:

    isActive:

    • true: La URL actual coincide con el to del link
    • false: No coincide
    • Ejemplo: URL /contacts/1, link a contacts/1 → isActive = true

    isPending:

    • true: El link fue clickeado, datos están cargando
    • false: No hay navegación pendiente o ya cargó
    • Proporciona feedback inmediato al usuario
  3. Lógica de clases:
    isActive ? "active" : isPending ? "pending" : ""
    
    • Si está activo → clase “active”
    • Si está pendiente → clase “pending”
    • Si ninguno → sin clase
  4. CSS correspondiente (en index.css):
    a.active {
      background-color: #e3e3e3;
      color: #000;
    }
       
    a.pending {
      color: #999;
    }
    

Ventajas de NavLink sobre Link:


13. Global Pending UI (Interfaz de Carga Global)

Problema de UX

Al navegar entre contactos, especialmente con conexión lenta:

Hook useNavigation

React Router expone el estado de navegación a través del hook useNavigation.

// src/routes/root.jsx
import {
  Outlet,
  NavLink,
  useLoaderData,
  Form,
  redirect,
  useNavigation,  // ← Nuevo import
} from "react-router-dom";

export default function Root() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();  // ← Usar el hook

  return (
    <>
      <div id="sidebar">
        {/* código del sidebar */}
      </div>
      <div
        id="detail"
        className={
          navigation.state === "loading" ? "loading" : ""
        }
      >
        <Outlet />
      </div>
    </>
  );
}

Análisis del código:

  1. Hook useNavigation:
    const navigation = useNavigation();
    
    • Retorna objeto con información de navegación
    • Se actualiza automáticamente durante transiciones
  2. Estados posibles de navigation.state:

    “idle”:

    • No hay navegación en curso
    • Estado normal de la aplicación
    • Todos los datos están cargados

    “loading”:

    • Navegación iniciada
    • Loaders están ejecutándose
    • Esperando datos

    “submitting”:

    • Formulario fue enviado
    • Action está ejecutándose
    • Procesando mutación
  3. Aplicación condicional de clase:
    className={navigation.state === "loading" ? "loading" : ""}
    
    • Si está cargando → agrega clase “loading”
    • Si no → sin clase adicional
  4. CSS para el estado loading (en index.css):
    #detail.loading {
      opacity: 0.25;
      transition: opacity 200ms;
      transition-delay: 200ms;
    }
    
    • opacity: 0.25: Atenúa el contenido
    • transition: 200ms: Animación suave
    • transition-delay: 200ms: Evita flicker en cargas rápidas

¿Por qué el delay?

transition-delay: 200ms;

Otras Propiedades de useNavigation

const navigation = useNavigation();

// Estado actual
navigation.state // "idle" | "loading" | "submitting"

// Información de la navegación
navigation.location // Location object de destino
navigation.formData // FormData si es submit de formulario
navigation.formAction // Action URL del formulario
navigation.formMethod // Método del formulario ("get" | "post")

Ejemplos de uso avanzado:

// Mostrar spinner global
{navigation.state === "loading" && <GlobalSpinner />}

// Deshabilitar form durante submit
<button 
  disabled={navigation.state === "submitting"}
>
  {navigation.state === "submitting" ? "Saving..." : "Save"}
</button>

// Barra de progreso
{navigation.state !== "idle" && <ProgressBar />}

Concepto: Optimistic UI

Aunque no se implementa en este tutorial, useNavigation también permite crear Optimistic UI:

function ContactList() {
  const navigation = useNavigation();
  const contacts = useLoaderData();
  
  // Mostrar datos optimistas durante submit
  const displayContacts = navigation.formData
    ? [...contacts, parseFormData(navigation.formData)]
    : contacts;
    
  return displayContacts.map(contact => ...);
}

Notas Importantes Sobre el Tutorial

Caché del Cliente

El tutorial menciona algo importante:

“Note that our data model (src/contacts.js) has a clientside cache, so navigating to the same contact is fast the second time.”

¿Qué significa esto?

Comportamiento de React Router:

Navegación: /contacts/1 → /contacts/2
- Re-ejecuta loader de contact (cambió)
- NO re-ejecuta loader de root (no cambió)

Próximos Pasos del Tutorial

El tutorial continúa con temas avanzados:


Resumen de Conceptos Clave

1. Enrutamiento Básico

2. Manejo de Errores

3. Navegación

4. Layouts Anidados

5. Carga de Datos

6. Mutación de Datos

7. Parámetros de URL

8. Estado de Navegación


Comparación: Antes y Después de React Router

Enfoque Tradicional (Sin React Router)

function App() {
  const [page, setPage] = useState('home');
  const [contacts, setContacts] = useState([]);
  const [currentContact, setCurrentContact] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchContacts();
  }, []);

  async function fetchContacts() {
    setLoading(true);
    const data = await fetch('/api/contacts');
    setContacts(data);
    setLoading(false);
  }

  async function createContact() {
    await fetch('/api/contacts', { method: 'POST' });
    await fetchContacts(); // Re-cargar manualmente
    setPage('list');
  }

  function navigateTo(page, contactId) {
    setPage(page);
    if (contactId) {
      setCurrentContact(contacts.find(c => c.id === contactId));
    }
  }

  return (
    <div>
      {page === 'list' && <ContactList contacts={contacts} />}
      {page === 'detail' && <ContactDetail contact={currentContact} />}
      {page === 'edit' && <ContactEdit contact={currentContact} />}
    </div>
  );
}

Problemas:

Enfoque con React Router

// main.jsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
      },
      {
        path: "contacts/:contactId/edit",
        element: <EditContact />,
        loader: contactLoader,
        action: editAction,
      },
    ],
  },
]);

// Componentes simples sin lógica de navegación
function ContactList() {
  const { contacts } = useLoaderData(); // Datos automáticos
  return contacts.map(contact => (
    <Link to={`contacts/${contact.id}`}>{contact.name}</Link>
  ));
}

Ventajas:


Patrones Comunes y Mejores Prácticas

1. Organización de Archivos

src/
├── routes/
│   ├── root.jsx
│   │   ├── export loader
│   │   ├── export action
│   │   └── export default Component
│   ├── contact.jsx
│   │   ├── export loader
│   │   └── export default Component
│   └── edit.jsx
│       ├── export loader
│       ├── export action
│       └── export default Component
├── components/
│   ├── ContactCard.jsx
│   ├── SearchBar.jsx
│   └── ... (componentes reutilizables)
├── api/
│   └── contacts.js (funciones de API)
└── main.jsx (configuración del router)

Principios:

2. Nomenclatura de Loaders y Actions

// ✅ BIEN: Nombres descriptivos al importar
import Root, { 
  loader as rootLoader, 
  action as rootAction 
} from "./routes/root";

import Contact, { 
  loader as contactLoader 
} from "./routes/contact";

import EditContact, { 
  loader as editContactLoader,
  action as editContactAction 
} from "./routes/edit";

// ❌ MAL: Conflictos de nombres
import { loader, action } from "./routes/root";
import { loader, action } from "./routes/contact"; // Error!

3. Manejo de Errores Específicos

// En loader o action
export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  
  return { contact };
}

// En errorElement
export default function ErrorPage() {
  const error = useRouteError();
  
  if (error.status === 404) {
    return <div>Contacto no encontrado</div>;
  }
  
  if (error.status === 401) {
    return <div>No autorizado</div>;
  }
  
  return <div>Error: {error.message}</div>;
}

4. Validación de Formularios

export async function action({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  
  // Validación
  const errors = {};
  if (!updates.first && !updates.last) {
    errors.name = "Debe proporcionar al menos un nombre";
  }
  
  if (updates.email && !isValidEmail(updates.email)) {
    errors.email = "Email inválido";
  }
  
  if (Object.keys(errors).length > 0) {
    return { errors }; // Retornar errores en lugar de redirect
  }
  
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}

// En el componente
function EditContact() {
  const { contact } = useLoaderData();
  const actionData = useActionData(); // Datos retornados por action
  
  return (
    <Form method="post">
      <input name="first" defaultValue={contact.first} />
      {actionData?.errors?.name && (
        <span className="error">{actionData.errors.name}</span>
      )}
      <button type="submit">Save</button>
    </Form>
  );
}

5. Loading States Granulares

function ContactList() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();
  
  // Detectar si estamos navegando a un contacto específico
  const isLoadingContact = navigation.state === "loading" 
    && navigation.location.pathname.includes("/contacts/");
  
  return (
    <ul>
      {contacts.map(contact => (
        <li 
          key={contact.id}
          className={isLoadingContact ? "dim" : ""}
        >
          <NavLink to={`contacts/${contact.id}`}>
            {contact.name}
          </NavLink>
        </li>
      ))}
    </ul>
  );
}

6. Reutilización de Datos entre Rutas

// Si múltiples rutas necesitan los mismos datos
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader, // Carga lista de contactos
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader, // Carga detalles
        children: [
          {
            path: "edit",
            element: <EditContact />,
            // No necesita loader, usa datos del padre
            action: editAction,
          }
        ]
      },
    ],
  },
]);

// EditContact puede acceder a datos del contactLoader del padre
function EditContact() {
  // useLoaderData busca hacia arriba en la jerarquía
  const { contact } = useLoaderData();
  return <Form>...</Form>;
}

7. Prevención de Navegación (Datos sin Guardar)

function EditContact() {
  const { contact } = useLoaderData();
  const [isDirty, setIsDirty] = useState(false);
  
  // Advertir antes de salir si hay cambios sin guardar
  useEffect(() => {
    if (isDirty) {
      const handleBeforeUnload = (e) => {
        e.preventDefault();
        e.returnValue = '';
      };
      window.addEventListener('beforeunload', handleBeforeUnload);
      return () => window.removeEventListener('beforeunload', handleBeforeUnload);
    }
  }, [isDirty]);
  
  return (
    <Form 
      method="post"
      onChange={() => setIsDirty(true)}
    >
      {/* inputs */}
    </Form>
  );
}

Debugging y Herramientas de Desarrollo

1. React DevTools

2. Network Tab

Antes de client-side routing:

Después de client-side routing:

3. Console Logs Útiles

// En loaders
export async function loader({ params, request }) {
  console.log('Loading contact:', params.contactId);
  console.log('URL:', request.url);
  const data = await getContact(params.contactId);
  console.log('Loaded:', data);
  return { contact: data };
}

// En actions
export async function action({ request, params }) {
  const formData = await request.formData();
  console.log('Form data:', Object.fromEntries(formData));
  // ... resto del código
}

// En componentes
function Root() {
  const navigation = useNavigation();
  console.log('Navigation state:', navigation.state);
  // ... resto del código
}

4. Errores Comunes y Soluciones

Error: “useLoaderData must be used within a data router”

// ❌ MAL: Usar useLoaderData fuera de un componente de ruta
function Sidebar() {
  const data = useLoaderData(); // Error!
}

// ✅ BIEN: Solo en componentes configurados como element
function Root() {
  const data = useLoaderData(); // OK
  return <Sidebar data={data} />; // Pasar como prop
}

Error: “No routes matched location”

// ❌ MAL: Path incorrecto
{
  path: "/contacts/:contactId",
  // ...
}
<Link to="/contact/1"> // Falta la 's'

// ✅ BIEN: Paths coinciden
<Link to="/contacts/1">

Error: “Cannot read property of undefined”

// ❌ MAL: No manejar datos vacíos
function Contact() {
  const { contact } = useLoaderData();
  return <div>{contact.name}</div>; // Error si contact es null
}

// ✅ BIEN: Usar optional chaining
function Contact() {
  const { contact } = useLoaderData();
  return <div>{contact?.name || 'No name'}</div>;
}

Conceptos Avanzados (Introducción)

Estos temas se cubren en partes posteriores del tutorial:

1. URL Search Params

// URL: /contacts?q=john&sort=name
export async function loader({ request }) {
  const url = new URL(request.url);
  const query = url.searchParams.get("q");
  const sort = url.searchParams.get("sort");
  
  const contacts = await getContacts(query, sort);
  return { contacts, query };
}

2. Formularios con GET

// Para búsquedas, usar GET en lugar de POST
<Form method="get" action="/contacts">
  <input name="q" />
  <button type="submit">Search</button>
</Form>

// Actualiza la URL con query params
// /contacts?q=searchterm

3. Optimistic UI

function ContactList() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();
  
  // Mostrar cambios inmediatamente
  const optimisticContacts = navigation.formData
    ? updateContactsOptimistically(contacts, navigation.formData)
    : contacts;
  
  return optimisticContacts.map(...);
}

4. Fetchers

// Para mutaciones sin navegación
import { useFetcher } from "react-router-dom";

function Favorite({ contact }) {
  const fetcher = useFetcher();
  
  return (
    <fetcher.Form method="post" action={`/contacts/${contact.id}/favorite`}>
      <button>
        {fetcher.state === "submitting" ? "..." : "★"}
      </button>
    </fetcher.Form>
  );
}

5. Defer (Streaming)

// Renderizar UI mientras los datos cargan
export async function loader() {
  return defer({
    contacts: getContacts(), // Promise sin await
  });
}

function Root() {
  const { contacts } = useLoaderData();
  
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Await resolve={contacts}>
        {(loadedContacts) => (
          <ContactList contacts={loadedContacts} />
        )}
      </Await>
    </Suspense>
  );
}

Ejercicios Prácticos Sugeridos

Ejercicio 1: Agregar Campo de Teléfono

Modifica la aplicación para incluir un campo de teléfono en los contactos.

Pasos:

  1. Agregar input de teléfono en EditContact
  2. Actualizar validación en la action
  3. Mostrar teléfono en Contact
  4. Formatear número (opcional)

Ejercicio 2: Confirmación de Cancelación

Implementa confirmación al hacer clic en “Cancel” si hay cambios.

Pasos:

  1. Detectar cambios en el formulario
  2. Mostrar confirmación con window.confirm()
  3. Prevenir navegación si el usuario cancela

Ejercicio 3: Mensaje de Éxito

Muestra un mensaje temporal después de guardar.

Pasos:

  1. Retornar mensaje en la action
  2. Usar useActionData() en el componente
  3. Mostrar mensaje temporalmente (setTimeout)
  4. Ocultar mensaje automáticamente

Ejercicio 4: Ordenamiento de Contactos

Agrega capacidad de ordenar contactos por nombre o fecha.

Pasos:

  1. Agregar selector de ordenamiento
  2. Usar URL search params para el orden
  3. Ordenar en el loader
  4. Mantener orden al navegar

Recursos Adicionales

Documentación Oficial

Conceptos Relacionados

Temas para Profundizar

  1. Server-Side Rendering (SSR) con React Router
  2. Code Splitting y lazy loading de rutas
  3. Route Guards y autenticación
  4. Data Fetching Patterns avanzados
  5. Testing de aplicaciones con React Router
  6. Migration de React Router v5 a v6
  7. Remix Framework (Next-gen con React Router)

Conclusión

React Router transforma el desarrollo de Single Page Applications al:

✅ Simplificar la navegación: Links declarativos y navegación automática

✅ Gestionar datos elegantemente: Loaders y actions eliminan boilerplate

✅ Mejorar la UX: Transiciones suaves, estado de loading, UI optimista

✅ Mantener URLs significativas: Cada vista tiene su propia URL única

✅ Reducir complejidad: Revalidación automática de datos

✅ Seguir estándares web: Basado en formularios HTML y History API

El Cambio de Paradigma

Antes: Gestionar manualmente estado, navegación, sincronización de datos

Después: Declarar rutas, loaders y actions; React Router gestiona el resto

Próximos Pasos en tu Aprendizaje

  1. ✅ Completar el tutorial oficial completo
  2. 📝 Practicar con los ejercicios sugeridos
  3. 🏗️ Construir una aplicación propia desde cero
  4. 📚 Explorar patrones avanzados
  5. 🚀 Estudiar Remix para apps full-stack

Glosario de Términos

Action: Función que procesa mutaciones de datos (POST, PUT, DELETE)

Browser Router: Router que usa la History API del navegador

Client-Side Routing: Navegación sin recargar la página

Deferred Data: Datos que se cargan mientras se renderiza la UI

Dynamic Segment: Parte variable de una URL (:param)

Error Boundary: Componente que captura errores de rutas

Fetcher: API para mutaciones sin navegación

FormData: API nativa para manejar datos de formularios

Loader: Función que carga datos antes de renderizar

NavLink: Link con capacidades de styling activo/pendiente

Nested Routes: Rutas hijas que se renderizan dentro de rutas padres

Outlet: Componente donde se renderizan rutas hijas

Optimistic UI: Actualizar UI antes de confirmar con servidor

Params: Parámetros capturados de la URL

Pending State: Estado de transición durante carga

Redirect: Navegación programática a otra ruta

Revalidation: Re-ejecución de loaders después de mutaciones

Root Route: Ruta contenedora principal de la aplicación

SPA: Single Page Application

URL Search Params: Parámetros de query en la URL (?key=value)

useActionData: Hook para acceder a datos retornados por actions

useLoaderData: Hook para acceder a datos cargados por loaders

useNavigation: Hook para estado de transiciones de navegación

useRouteError: Hook para acceder a errores capturados


Tabla de Referencia Rápida

Hooks Principales

Hook Propósito Retorna
useLoaderData() Datos del loader Datos cargados
useActionData() Datos de la action Datos retornados
useNavigation() Estado de navegación { state, location, formData, ... }
useRouteError() Error capturado Objeto de error
useParams() Parámetros de URL { paramName: value, ... }
useSearchParams() Query params [searchParams, setSearchParams]

Componentes Principales

Componente Propósito Props Clave
<RouterProvider> Proveedor del router router
<Outlet> Renderiza rutas hijas -
<Link> Navegación básica to
<NavLink> Link con estados to, className
<Form> Formulario con routing method, action

Configuración de Rutas

{
  path: "/ruta/:param",        // URL path con params
  element: <Component />,       // Componente a renderizar
  loader: loaderFunction,       // Carga datos
  action: actionFunction,       // Procesa mutaciones
  errorElement: <Error />,      // Maneja errores
  children: [...],              // Rutas hijas
}

¡Felicitaciones! Has completado una guía exhaustiva de los fundamentos de React Router. Este documento cubre desde conceptos básicos hasta patrones avanzados, proporcionándote una base sólida para construir aplicaciones web modernas con enrutamiento del lado del cliente.

Recuerda: la mejor manera de aprender es practicando. Construye proyectos, experimenta con diferentes patrones, y no dudes en consultar la documentación oficial cuando necesites profundizar en temas específicos.