La lliçó anterior va deixar CicloUrbano enrutada però mutilada: cada pantalla es pinta sola, sense Capcalera i sense PeuDePagina, perquè Disseny es va quedar fora del mapa de rutes. La solució òbvia —embolcallar cada pàgina en <Disseny>— funciona i és dolenta: repeteix el mateix marcatge nou vegades, torna a muntar la capçalera a cada navegació i no permet que el marc comparteixi dades amb la pantalla. React Router proposa alguna cosa millor: les rutes s'imbriquen igual que els components, de manera que una ruta pare pinta el marc i reserva un forat on apareixerà la ruta filla activa. En aquesta lliçó aprendràs aquest mecanisme amb children i <Outlet />, convertiràs Disseny en la ruta arrel, distingiràs les rutes índex de les rutes amb path: '', usaràs rutes sense path per agrupar sense embrutar la URL, construiràs el detall d'estació amb pestanyes flota i incidencias, passaràs dades del pare a la filla amb useOutletContext, generaràs molles de pa a partir del propi mapa amb handle i useMatches, i gestionaràs errors per branca amb errorElement.

Contingut

  1. La idea: composició d'interfície a través de la URL
  2. children i <Outlet />
  3. Disseny com a ruta arrel: què millora exactament
  4. Rutes índex: index: true
  5. index: true enfront de path: ''
  6. Rutes sense path: agrupar sense afegir segment
  7. Rutes relatives i absolutes
  8. Cas complet: detall d'estació amb pestanyes
  9. useOutletContext: dades del pare a la filla
  10. Molles de pa amb handle i useMatches
  11. Errors per branca: errorElement i useRouteError
  12. errorElement enfront de LimitError
  13. Nota sobre loader i action

  1. La idea: composició d'interfície a través de la URL

Torna a mirar una URL de CicloUrbano i llegeix-la per trossos:

/estaciones/est-02/incidencias
 └────┬───┘ └──┬──┘ └────┬────┘
      │        │         └─ pestanya dins del detall
      │        └─ quina estació
      └─ secció de l'aplicació

Cada segment acota l'anterior. I si t'hi fixes, la interfície que correspon a aquesta URL té exactament la mateixa estructura de nines russes: el marc de l'aplicació conté la pantalla d'estacions, que conté el detall d'est-02, que conté la pestanya d'incidències.

Imbricar rutes consisteix a declarar que una ruta és filla d'una altra, de manera que quan la filla estigui activa el pare també es renderitza, i la filla apareix dins seu.

Aquesta correspondència entre la jerarquia de la URL i la jerarquia de components és el que fa que l'enrutament a React encaixi tan bé amb el que ja saps de composició (04-02): un mapa de rutes imbricat és un arbre de components, escrit d'una altra manera.

flowchart TD
    subgraph URL["URL: /estaciones/est-02/incidencias"]
        S1["/estaciones"] --> S2["/est-02"] --> S3["/incidencias"]
    end
    subgraph UI["Interfície renderitzada"]
        C0["Disseny<br/>Capcalera + Outlet + PeuDePagina"]
        C0 --> C1["PaginaDetallEstacio<br/>títol + pestanyes + Outlet"]
        C1 --> C2["PestanyaIncidencies"]
    end
    S1 -.-> C0
    S2 -.-> C1
    S3 -.-> C2
    style C0 fill:#e0f2fe
    style C1 fill:#fef9c3
    style C2 fill:#dcfce7

  1. children i <Outlet />

Dues peces i ja està. Al mapa, la propietat children; al component pare, el component <Outlet />.

// src/rutes.jsx — la forma més simple d'imbricació
import { createBrowserRouter } from 'react-router';
import Disseny from './components/Disseny.jsx';
import PaginaCataleg from './pagines/PaginaCataleg.jsx';
import PaginaEstacions from './pagines/PaginaEstacions.jsx';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Disseny />,          // ← es pinta SEMPRE
    children: [
      { index: true, element: <PaginaCataleg /> },
      { path: 'estaciones', element: <PaginaEstacions /> }
    ]
  }
]);
// src/components/Disseny.jsx — de children a Outlet
import { Outlet } from 'react-router';
import Capcalera from './Capcalera.jsx';
import PeuDePagina from './PeuDePagina.jsx';
import estils from './Disseny.module.css';

function Disseny() {
  return (
    <div className={estils.disseny}>
      <Capcalera />
      <main className={estils.principal}>
        <Outlet />          {/* ← aquí es pinta la ruta filla activa */}
      </main>
      <PeuDePagina />
    </div>
  );
}

export default Disseny;

Compara-ho amb la versió de 04-02, que rebia { children }:

Disseny amb children (04-02) Disseny amb <Outlet /> (ara)
Qui decideix el contingut Qui escriu <Disseny>…</Disseny> L'enrutador, segons la URL
Signatura function Disseny({ children }) function Disseny()
On s'utilitza Manualment a App Com a element d'una ruta
Quan canvia el contingut Quan el pare repinta En navegar

<Outlet /> és el children de l'enrutador. És la mateixa idea de forat de 04-02, però emplenat automàticament per la ruta filla que coincideix amb la URL. Fixa't que Disseny ja no accepta children: si necessites totes dues coses —i a vegades passa—, pots tenir-les totes dues, però a CicloUrbano no cal.

I les regles que governen la imbricació:

  • Els path de les filles són relatius al pare i no porten / inicial. La filla path: 'estaciones' dins del pare path: '/' produeix la URL /estaciones. Escriure path: '/estaciones' a la filla també funciona (és absoluta), però trenca el patró i complica reorganitzar l'arbre.
  • Si el pare no té <Outlet />, la filla no es veu. És l'error més freqüent d'aquesta lliçó: tot està ben configurat, la URL coincideix, però la pantalla no apareix perquè falta el forat.
  • Es pot imbricar sense límit de profunditat, tot i que més de tres o quatre nivells sol indicar que el model d'URLs s'ha complicat de més.

  1. Disseny com a ruta arrel: què millora exactament

Aquest és el mapa complet de CicloUrbano ja imbricat, substituint el plànol de 06-02:

// src/rutes.jsx
import { createBrowserRouter } from 'react-router';

import Disseny from './components/Disseny.jsx';
import PaginaCataleg from './pagines/PaginaCataleg.jsx';
import PaginaFitxaBicicleta from './pagines/PaginaFitxaBicicleta.jsx';
import PaginaEstacions from './pagines/PaginaEstacions.jsx';
import PaginaDetallEstacio from './pagines/PaginaDetallEstacio.jsx';
import PestanyaFlota from './pagines/PestanyaFlota.jsx';
import PestanyaIncidencies from './pagines/PestanyaIncidencies.jsx';
import PaginaReserves from './pagines/PaginaReserves.jsx';
import PaginaNovaReserva from './pagines/PaginaNovaReserva.jsx';
import PaginaAcces from './pagines/PaginaAcces.jsx';
import PaginaTaller from './pagines/PaginaTaller.jsx';
import PaginaNoTrobada from './pagines/PaginaNoTrobada.jsx';
import PaginaErrorRuta from './pagines/PaginaErrorRuta.jsx';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Disseny />,
    errorElement: <PaginaErrorRuta />,
    handle: { molla: 'Inici' },
    children: [
      { index: true, element: <PaginaCataleg />, handle: { molla: 'Catàleg' } },

      {
        path: 'bicicletas/:bicicletaId',
        element: <PaginaFitxaBicicleta />,
        handle: { molla: 'Fitxa de bicicleta' }
      },

      {
        path: 'estaciones',
        handle: { molla: 'Estacions' },
        children: [
          { index: true, element: <PaginaEstacions /> },
          {
            path: ':estacionId',
            element: <PaginaDetallEstacio />,
            handle: { molla: 'Detall d\'estació' },
            children: [
              { index: true, element: <PestanyaFlota /> },
              { path: 'incidencias', element: <PestanyaIncidencies /> }
            ]
          }
        ]
      },

      {
        path: 'reservas',
        handle: { molla: 'Les meves reserves' },
        children: [
          { index: true, element: <PaginaReserves /> },
          { path: 'nueva', element: <PaginaNovaReserva />, handle: { molla: 'Nova reserva' } }
        ]
      },

      { path: 'acceso', element: <PaginaAcces />, handle: { molla: 'Accés' } },
      { path: 'taller', element: <PaginaTaller />, handle: { molla: 'Taller' } },

      { path: '*', element: <PaginaNoTrobada /> }
    ]
  }
]);

Fixa't que la ruta estaciones no té element: només agrupa. Quan una ruta té filles i cap element, React Router hi pinta implícitament un <Outlet />, de manera que les filles pugen directament al forat de l'avi. És una manera neta d'organitzar el mapa per seccions.

Què millora exactament convertir Disseny en ruta arrel en lloc d'embolcallar cada pàgina:

<Disseny> al voltant de cada pàgina Disseny com a ruta arrel
Repetició del marcatge Nou vegades Una
En navegar entre pantalles Disseny, Capcalera i PeuDePagina es desmunten i es tornen a muntar Persisteixen: React només canvia el que hi ha a l'<Outlet />
Estat dins de la capçalera (menú obert, cercador) Es perd a cada navegació Es conserva
Efectes de la capçalera S'executen la seva neteja i el seu muntatge a cada clic S'executen una vegada
Posició del desplaçament d'una barra lateral llarga Torna amunt Es manté
Proveïdors col·locats allà (ProveidorAvisos, ProveidorReserves) Es reinicien: es perden els avisos i les reserves Sobreviuen a la navegació
Animacions d'entrada de la capçalera Es repeteixen molestament No es repeteixen

La fila dels proveïdors és la de més calat i explica una decisió pendent de 06-02. ProveidorReserves i ProveidorAvisos van dins de Disseny, no a main.jsx:

// src/components/Disseny.jsx — versió definitiva
import { Outlet } from 'react-router';
import { ProveidorReserves } from '../contextos/ContextReserves.jsx';
import { ProveidorAvisos } from '../contextos/ContextAvisos.jsx';
import Capcalera from './Capcalera.jsx';
import PeuDePagina from './PeuDePagina.jsx';
import LlistaAvisos from './LlistaAvisos.jsx';
import estils from './Disseny.module.css';

function Disseny() {
  return (
    <ProveidorReserves>
      <ProveidorAvisos>
        <div className={estils.disseny}>
          <Capcalera />
          <LlistaAvisos />           {/* els avisos es veuen des de qualsevol pantalla */}
          <main className={estils.principal}>
            <Outlet />
          </main>
          <PeuDePagina />
        </div>
      </ProveidorAvisos>
    </ProveidorReserves>
  );
}

export default Disseny;

El raonament és el de 06-02, un nivell més avall: ProveidorTema i ProveidorUsuari van fora de l'enrutador perquè han de sobreviure a absolutament tot; ProveidorReserves i ProveidorAvisos van a la ruta arrel perquè pertanyen al marc de l'aplicació, el consumeixen diverses pantalles i així queden al costat de la interfície que els usa. Com que Disseny no es desmunta en navegar, l'efecte pràctic és el mateix i el mapa es llegeix millor. Aquest detall és el que permetrà que a 06-04 creïs una reserva a /reservas/nueva i la vegis a /reservas: si el proveïdor es reiniciés en canviar de pantalla, la reserva s'hauria evaporat pel camí.

  1. Rutes índex: index: true

Pregunta: amb el pare path: 'estaciones' i la filla path: ':estacionId', què es veu a /estaciones a seques? Res dins de l'<Outlet />: cap filla coincideix.

Per a això hi ha la ruta índex:

Una ruta índex (index: true) és la filla que es renderitza a l'<Outlet /> del pare quan la URL coincideix exactament amb el pare i no hi ha més segments.

{
  path: 'estaciones',
  children: [
    { index: true, element: <PaginaEstacions /> },      // → /estaciones
    { path: ':estacionId', element: <PaginaDetallEstacio /> }  // → /estaciones/est-02
  ]
}
URL Ruta filla activa Què es veu a l'<Outlet />
/estaciones la índex PaginaEstacions
/estaciones/est-02 :estacionId PaginaDetallEstacio
/estaciones/est-99 :estacionId PaginaDetallEstacio → «no trobada»

Les seves regles:

  • Una ruta índex no té path. { index: true, path: 'algo' } és un error.
  • Una ruta índex no pot tenir children. És una fulla de l'arbre per definició.
  • Només n'hi pot haver una per pare.
  • És la pantalla per defecte d'aquesta secció, i per això dona nom al patró: és l'«índex» de la carpeta, com l'index.html d'un directori en un servidor web.

A CicloUrbano n'hi ha tres: el catàleg sota l'arrel, el llistat sota estaciones i la pestanya de flota sota el detall d'estació.

  1. index: true enfront de path: ''

Hi ha una alternativa que sembla equivalent i no ho és del tot:

{ index: true, element: <PaginaEstacions /> }
{ path: '', element: <PaginaEstacions /> }   // el mateix?

Totes dues coincideixen amb la URL del pare. Les diferències:

index: true path: ''
Coincideix amb la URL del pare
Pot tenir children No
Intenció declarada «Aquesta és la pantalla per defecte» «Aquesta ruta afegeix un marc sense canviar la URL»
Comportament en enllaços relatius Es comporta com el pare Compta com un nivell més
Recomanació Usa-la per a pantalles per defecte Només si necessites un nivell d'imbricació extra sense segment

A la pràctica: usa index: true gairebé sempre. path: '' és l'eina per a un cas concret —inserir un component intermedi que a més tingui filles pròpies— i aquí competeix amb la ruta sense path de l'apartat següent, que sol expressar-ho millor.

Un avís: path: '/' en una ruta filla no és el mateix que path: ''. La barra la converteix en absoluta i produeix coincidències sorprenents. Dins d'un pare, deixa les filles sempre relatives.

  1. Rutes sense path: agrupar sense afegir segment

Aquest és un patró que resol un problema molt concret: aplicar un marc comú a diverses pantalles sense que la URL ho reflecteixi.

Una ruta sense path (pathless route) és una ruta amb element i children però sense path. No consumeix cap segment de la URL: només insereix el seu element a l'arbre i deixa que les filles segueixin com si no hi fos.

{
  path: '/',
  element: <Disseny />,
  children: [
    { index: true, element: <PaginaCataleg /> },

    // Ruta SENSE path: embolcalla, però no apareix a la URL
    {
      element: <RutaProtegida />,
      children: [
        { path: 'taller', element: <PaginaTaller /> },        // → /taller
        { path: 'informes', element: <PaginaInformes /> }      // → /informes
      ]
    }
  ]
}

Fixa't en el resultat: /taller continua sent /taller, no /protegido/taller. El component RutaProtegida es renderitza entre Disseny i PaginaTaller, comprova la sessió i decideix si pinta el seu <Outlet /> o redirigeix. Aquest és el patró 2 de 06-05, i el veuràs allà desenvolupat.

Altres usos habituals:

Ús Què agrupa
Protecció Diverses pantalles darrere d'un mateix guardià de sessió
Gestió d'errors Diverses pantalles sota un mateix errorElement
Marc secundari Una barra lateral compartida per tres pantalles les URL de les quals no comparteixen prefix
Proveïdor acotat Un context que només ha d'existir en un grup de pantalles

La regla mental: path és per a la URL; la imbricació és per a la interfície. Que dues pantalles comparteixin marc no obliga que comparteixin prefix, i una ruta sense path és justament la manera de desacoblar totes dues coses.

  1. Rutes relatives i absolutes

A path i a to, una cadena que comença per / és absoluta i una que no, relativa. La diferència importa més del que sembla.

A path (dins del mapa):

{
  path: 'estaciones',
  children: [
    { path: ':estacionId', … },    // ✅ relativa → /estaciones/est-02
    { path: '/estaciones/:estacionId', … }  // ⚠️ absoluta: funciona, però fràgil
  ]
}

La relativa es recomana perquè si demà renombres la secció a paradas, canvies una línia i tot el subarbre continua funcionant. Amb rutes absolutes hauries d'editar cada filla.

A to (dins dels components): la referència és la ruta activa, no la URL literal. Estant a /estaciones/est-02:

to Destinació Comentari
"/" / Absoluta: des de l'arrel
"/estaciones" /estaciones Absoluta
"incidencias" /estaciones/est-02/incidencias Relativa: s'afegeix al final
".." /estaciones Puja un nivell de ruta
"../est-01" /estaciones/est-01 Puja i baixa

El cas de .. té una subtilesa que convé conèixer: per defecte puja un nivell de la jerarquia de rutes, no de segments de URL. Si vols el comportament de segments, com en un sistema de fitxers, s'ha de demanar explícitament:

<Link to=".." relative="path">Pujar un segment de URL</Link>

Consell pràctic: usa rutes relatives dins d'un subarbre (les pestanyes d'una pantalla, la navegació interna d'una secció) i absolutes per a la navegació principal (el menú de Capcalera). Les relatives fan que un subarbre es pugui moure de lloc sense tocar els seus enllaços; les absolutes deixen clara la destinació al menú global.

  1. Cas complet: detall d'estació amb pestanyes

Anem amb el cas central de la lliçó. /estaciones/:estacionId mostra les dades de l'estació i dues pestanyes: flota (per defecte) i incidencias. Cada pestanya és una ruta, així que té la seva pròpia URL, es pot compartir i el botó enrere alterna entre elles.

// src/pagines/PaginaDetallEstacio.jsx
import { useParams, NavLink, Outlet, Link } from 'react-router';
import { estacions, bicicletes } from '../dades/domini.js';
import PaginaNoTrobada from './PaginaNoTrobada.jsx';
import estils from './PaginaDetallEstacio.module.css';

function PaginaDetallEstacio() {
  const { estacionId } = useParams();
  const estacio = estacions.find((est) => est.id === estacionId);

  if (!estacio) {
    return <PaginaNoTrobada recurs="estació" identificador={estacionId} />;
  }

  // Es calcula UNA vegada aquí i es comparteix amb les pestanyes
  const flota = bicicletes.filter((bici) => bici.estacioId === estacio.id);

  const classesPestanya = ({ isActive }) =>
    isActive ? `${estils.pestanya} ${estils.activa}` : estils.pestanya;

  return (
    <article>
      <p className={estils.mollaSimple}>
        <Link to="/estaciones">← Estacions</Link>
      </p>

      <h2>{estacio.nom}</h2>
      <p>
        Barri de {estacio.barri} · {estacio.places} places ·{' '}
        {flota.length} bicicletes assignades
      </p>

      {/* Les pestanyes són NavLink relatius: no repeteixen l'id de l'estació */}
      <nav className={estils.pestanyes} aria-label="Seccions de l'estació">
        <NavLink to="." end className={classesPestanya}>
          Flota
        </NavLink>
        <NavLink to="incidencias" className={classesPestanya}>
          Incidències
        </NavLink>
      </nav>

      {/* La pestanya activa es pinta aquí, i rep la flota ja calculada */}
      <Outlet context={{ estacio, flota }} />
    </article>
  );
}

export default PaginaDetallEstacio;

Detalls que fan que això funcioni:

  • to="." apunta a la pròpia ruta activa, és a dir, a /estaciones/est-02. Amb end exigeix coincidència exacta, de manera que estant a /incidencias la pestanya «Flota» no apareix marcada. Sense end, totes dues es veurien actives alhora, que és l'error clàssic de les pestanyes imbricades.
  • to="incidencias" és relatiu: s'afegeix al final de la ruta actual. No escriguis mai aquí /estaciones/est-02/incidencias, perquè hauries d'interpolar l'identificador a mà i el component deixaria de ser reutilitzable.
  • La flota es calcula una vegada al pare i baixa a les filles per context, en lloc que cada pestanya torni a filtrar l'array. És l'apartat següent.
  • La navegació entre pestanyes no torna a muntar la capçalera de l'estació: l'<article>, el títol i les pestanyes persisteixen; només canvia el que hi ha dins de l'<Outlet />. Exactament el mateix guany que amb Disseny, un nivell més avall.
flowchart TD
    D["Disseny<br/><i>Capcalera · Outlet · PeuDePagina</i>"]
    D --> P["PaginaDetallEstacio<br/><i>títol · pestanyes · Outlet</i>"]
    P --> F["PestanyaFlota<br/><b>ruta índex</b><br/>/estaciones/est-02"]
    P --> I["PestanyaIncidencies<br/>/estaciones/est-02/incidencias"]
    style D fill:#e0f2fe
    style P fill:#fef9c3
    style F fill:#dcfce7
    style I fill:#dcfce7

  1. useOutletContext: dades del pare a la filla

Problema real: PestanyaFlota i PestanyaIncidencies necessiten l'estació i la seva flota. Podrien llegir useParams() i tornar a buscar a domini.js, però això duplicaria la feina, duplicaria també la comprovació de «no trobada» i —quan les dades vinguin d'una API a 07-06— duplicaria la petició.

<Outlet /> accepta una prop context amb el que vulguis passar, i la filla ho llegeix amb useOutletContext():

// src/pagines/PestanyaFlota.jsx
import { useOutletContext, Link } from 'react-router';
import EtiquetaEstat from '../components/EtiquetaEstat.jsx';

function PestanyaFlota() {
  const { estacio, flota } = useOutletContext();

  if (flota.length === 0) {
    return <p>No hi ha bicicletes assignades a {estacio.nom} ara mateix.</p>;
  }

  return (
    <ul>
      {flota.map((bici) => (
        <li key={bici.id}>
          <Link to={`/bicicletas/${bici.id}`}>{bici.model}</Link>{' '}
          <EtiquetaEstat estat={bici.estat} />{' '}
          <span>{bici.preuHora.toFixed(2)} €/h</span>
        </li>
      ))}
    </ul>
  );
}

export default PestanyaFlota;
// src/pagines/PestanyaIncidencies.jsx
import { useOutletContext } from 'react-router';
import Avis from '../components/Avis.jsx';

function PestanyaIncidencies() {
  const { estacio, flota } = useOutletContext();

  const enManteniment = flota.filter((bici) => bici.estat === 'mantenimiento');

  if (enManteniment.length === 0) {
    return <p>{estacio.nom} no té incidències obertes.</p>;
  }

  return (
    <ul>
      {enManteniment.map((bici) => (
        <li key={bici.id}>
          <Avis to="advertencia" titol={`${bici.model} en manteniment`}>
            <p>La bicicleta {bici.id} està retirada del servei a {estacio.nom}.</p>
          </Avis>
        </li>
      ))}
    </ul>
  );
}

export default PestanyaIncidencies;

useOutletContext enfront de useContext —perquè el dubte és legítim després de 05-04:

useOutletContext Context de React (useContext)
Abast Només la ruta filla directa de l'<Outlet /> Tot el subarbre del proveïdor
Com es declara <Outlet context={…} /> <ProveidorX> amb createContext
Quan usar-lo Dades d'aquesta pantalla per a les seves pestanyes Dades ambientals per a mitja aplicació
Exemple a CicloUrbano { estacio, flota } usuari, tema, avisos, reserves
Verbós Res: una prop Fitxer de context + proveïdor + hook

La regla: useOutletContext per al que és local de la ruta; context de React per al que és global. Passar l'usuari per useOutletContext des de Disseny seria tècnicament possible, però obligaria que cada nivell intermedi el reenviés: la perforació de props que 05-04 va venir a eliminar.

Un avís de mantenibilitat: useOutletContext no avisa si el pare no ha posat context; retorna null i la desestructuració rebenta amb un missatge poc informatiu. Si l'objecte és important, protegeix-lo com fas amb els hooks de context del projecte:

const context = useOutletContext();
if (!context) {
  throw new Error('PestanyaFlota s\'ha de renderitzar dins de PaginaDetallEstacio');
}

  1. Molles de pa amb handle i useMatches

Amb l'arbre de rutes imbricat tens gratis la informació necessària per a unes molles de pa: la cadena de rutes actives ja descriu on és l'usuari. useMatches() retorna aquesta cadena, de l'arrel a la fulla.

Cada element de la cadena té:

Propietat Contingut
id Identificador intern de la ruta ('0-1-2' o l'id que li posis)
pathname La part de la URL que li correspon: /estaciones/est-02
params Els paràmetres acumulats fins allà
handle El que tu hagis posat a la ruta: camp lliure
data El que retorna el seu loader, si en té

handle és la clau: un camp sense significat per a React Router i amb el significat que tu decideixis. Al mapa de l'apartat 3 ja l'hem emplenat amb { molla: '…' }.

// src/components/MollesDePa.jsx
import { useMatches, Link } from 'react-router';
import estils from './MollesDePa.module.css';

function MollesDePa() {
  const coincidencies = useMatches();

  // Només interessen les rutes que han declarat una molla
  const molles = coincidencies.filter((c) => Boolean(c.handle?.molla));

  if (molles.length <= 1) return null;   // a la portada no aporta res

  return (
    <nav aria-label="Molles de pa" className={estils.molles}>
      <ol>
        {molles.map((coincidencia, posicio) => {
          const esUltima = posicio === molles.length - 1;

          // La molla pot ser text o una funció dels paràmetres
          const text =
            typeof coincidencia.handle.molla === 'function'
              ? coincidencia.handle.molla(coincidencia.params)
              : coincidencia.handle.molla;

          return (
            <li key={coincidencia.id}>
              {esUltima ? (
                <span aria-current="page">{text}</span>
              ) : (
                <Link to={coincidencia.pathname}>{text}</Link>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

export default MollesDePa;

La molla com a funció permet molles dinàmiques que usen els paràmetres:

// A src/rutes.jsx
import { estacions } from './dades/domini.js';

{
  path: ':estacionId',
  element: <PaginaDetallEstacio />,
  handle: {
    molla: (params) =>
      estacions.find((est) => est.id === params.estacionId)?.nom ?? 'Estació'
  },
  children: [ /* … */ ]
}

Amb això, a /estaciones/est-02/incidencias les molles diuen Inici › Estacions › Parc Nord, i cadascuna és un enllaç real. Col·loca <MollesDePa /> a Disseny, just a sobre de l'<Outlet />, i funcionarà a totes les pantalles sense tocar-ne cap:

<main className={estils.principal}>
  <MollesDePa />
  <Outlet />
</main>

Aquest és l'avantatge del mode de dades que anunciava 06-01: el mapa de rutes és una dada, i per tant es pot recórrer per generar interfície. Amb <Routes> i <Route> en JSX això seria molt més incòmode.

  1. Errors per branca: errorElement i useRouteError

A 04-05 vas construir LimitError, l'única classe del projecte, capaç de capturar els errors de renderitzat del seu subarbre. El mode de dades de React Router afegeix un mecanisme paral·lel i més fi: cada ruta pot declarar el seu propi errorElement.

{
  path: '/',
  element: <Disseny />,
  errorElement: <PaginaErrorRuta />,   // ← captura els fallats de tota aquesta branca
  children: [ /* … */ ]
}

Quan alguna cosa falla dins d'aquesta branca, React Router substitueix l'element de la ruta més propera que tingui errorElement per aquest element, deixant intactes els seus ancestres. El component llegeix l'error amb useRouteError():

// src/pagines/PaginaErrorRuta.jsx
import { useRouteError, isRouteErrorResponse, Link } from 'react-router';
import Avis from '../components/Avis.jsx';
import { registrarError } from '../utilitats/monitoritzacio.js';

function PaginaErrorRuta() {
  const error = useRouteError();

  // Cas 1: una Response llançada a propòsit (404, 403…)
  if (isRouteErrorResponse(error)) {
    return (
      <section>
        <Avis to="error" titol={`Error ${error.status}: ${error.statusText}`}>
          <p>{error.data || 'No hem pogut completar l\'operació.'}</p>
        </Avis>
        <Link to="/">Tornar al catàleg</Link>
      </section>
    );
  }

  // Cas 2: un error de JavaScript no previst
  registrarError(error, 'errorElement de la ruta arrel');

  return (
    <section>
      <Avis to="error" titol="Alguna cosa ha fallat en aquesta pantalla">
        <p>
          L'equip ja ha rebut l'avís. Pots tornar al catàleg i continuar
          usant CicloUrbano amb normalitat.
        </p>
        {import.meta.env.DEV && <pre>{error?.message ?? String(error)}</pre>}
      </Avis>
      <Link to="/">Tornar al catàleg</Link>
    </section>
  );
}

export default PaginaErrorRuta;

Punts importants:

  • isRouteErrorResponse distingeix un error «esperat» —una Response que has llançat tu— d'una excepció de JavaScript. Els dos casos mereixen missatges diferents: el primer és una situació prevista; el segon, un fallat.
  • Llançar una Response és la tercera estratègia que 06-02 esmentava per a un recurs inexistent:
const bicicleta = bicicletes.find((bici) => bici.id === bicicletaId);
if (!bicicleta) {
  throw new Response('Bicicleta no trobada', { status: 404 });
}
  • El detall tècnic només en desenvolupament. import.meta.env.DEV és la variable de Vite (01-02). En producció, el missatge a l'usuari; la traça, a registrarError.
  • errorElement per branca. Si en poses un a la ruta estaciones, un fallat al detall d'una estació es conté allà: la capçalera, el peu i les molles continuen vius, i l'usuari pot navegar a una altra secció sense recarregar.

  1. errorElement enfront de LimitError

Tens dos mecanismes de contenció i convé saber què cobreix cadascun. No competeixen: es complementen.

LimitError (04-05) errorElement (React Router)
Què és Component de classe propi Propietat d'una ruta
On es col·loca En qualsevol punt de l'arbre En una ruta del mapa
Captura errors de renderitzat Sí, dins de la seva branca
Captura errors de loader/action No
Captura Response llançades No les distingeix Sí, amb isRouteErrorResponse
Captura errors de gestors d'esdeveniments No No
Captura errors asíncrons (setTimeout, promeses) No No
Es reinicia en navegar No, cal reintentar a mà : en canviar de ruta desapareix
Abast típic L'aplicació sencera, o un panell concret Una secció de l'aplicació
Disponible en mode declaratiu No: només en el mode de dades

Aquesta fila de «es reinicia en navegar» és l'avantatge pràctic més notable. Amb LimitError, un cop capturat el fallat el subarbre queda substituït fins que algú premi «Reintentar». Amb errorElement, n'hi ha prou que l'usuari navegui a una altra ruta perquè tot torni a la normalitat, que és el comportament que espera.

La configuració recomanada per a CicloUrbano combina totes dues:

flowchart TD
    ARREL["main.jsx"] --> LE["LimitError GLOBAL<br/><i>xarxa de seguretat última:<br/>fallats del propi enrutador</i>"]
    LE --> PROV["ProveidorTema · ProveidorUsuari"]
    PROV --> RP["RouterProvider"]
    RP --> R0["Ruta / → Disseny<br/><b>errorElement: PaginaErrorRuta</b>"]
    R0 --> R1["Ruta estaciones<br/><i>pot tenir el seu propi errorElement</i>"]
    R0 --> R2["Ruta reservas"]
    R1 --> R3["PaginaDetallEstacio 💥"]
    R3 -. "l'error puja a l'<br/>errorElement més proper" .-> R1
    style LE fill:#fde68a
    style R0 fill:#e0f2fe
    style R3 fill:#fecaca

I continua vigent la limitació de 04-05, que cap dels dos resol: els errors llançats dins d'un gestor d'esdeveniments no els captura ningú. Allà continua fent falta try/catch i un avís a l'usuari a través de useAvisos.

  1. Nota sobre loader i action

El mode de dades es diu així per dues propietats de ruta que aquesta lliçó no desenvolupa:

{
  path: 'estaciones/:estacionId',
  element: <PaginaDetallEstacio />,
  loader: async ({ params }) => obtenirEstacio(params.estacionId),  // dades ABANS de renderitzar
  action: async ({ request }) => desarIncidencia(await request.formData())  // enviaments
}

La idea és potent: el loader carrega les dades abans de pintar la pantalla, i així s'evita el patró «renderitzar, llançar un efecte, mostrar un indicador de càrrega, repintar» amb el qual has treballat des de 05-02, juntament amb les cascades de peticions que produeix. useLoaderData() recupera el resultat a la pàgina.

No l'usarem aquí per una raó d'ordre: és una decisió d'arquitectura de dades, no d'enrutament, i competeix amb les biblioteques d'estat del servidor. A 07-06 compararàs loader/action amb TanStack Query i decidiràs; i a 10-01, amb Next.js, veuràs la versió d'aquesta mateixa idea portada al servidor. De moment, CicloUrbano continua llegint de domini.js i de useFetchBicicletes, que és suficient i manté el focus.

Errors Comuns i Consells

Oblidar <Outlet /> al component pare. El símptoma és exasperant: la ruta coincideix, no hi ha cap error a la consola, i la pantalla filla simplement no apareix. Si una ruta té children, el seu element ha de contenir un <Outlet />.

Posar / inicial al path d'una filla. { path: '/estaciones' } dins del pare { path: '/' } és absoluta: funciona per casualitat, però deixa de funcionar tan bon punt moguis el subarbre. Les filles van sense barra inicial.

Pestanyes totes actives alhora. Li falta end al NavLink de la pestanya índex (to="."). Sense ell, la pestanya per defecte es considera activa també a les germanes, perquè la seva destinació és prefix de totes.

Interpolar la URL completa a les pestanyes. to={/estaciones/${estacionId}/incidencias} funciona, però obliga a arrossegar l'identificador i es trenca si canvies el mapa. Usa to="incidencias" relatiu.

Confondre index: true amb una ruta que coincideix sempre. La ruta índex es pinta només quan la URL coincideix exactament amb el pare. A /estaciones/est-02 la índex d'estaciones no està activa.

Usar useOutletContext sense que el pare passi context. Retorna null i la desestructuració llança un error confús. Comprova-ho, o si més no documenta el contracte en un comentari.

Recalcular a cada pestanya el que el pare ja té. Si PestanyaFlota i PestanyaIncidencies tornen a filtrar bicicletes, dupliques feina, dupliques la comprovació d'existència i —quan hi hagi API— dupliques les peticions. Calcula-ho al pare i baixa-ho per context.

Consell: reflecteix a la URL el que l'usuari voldria compartir. Una pestanya sí (/estaciones/est-02/incidencias té sentit enviat per xat); l'estat d'un acordió obert, no. Si l'estat no sobreviu a un F5 i a ningú li importa, és useState; si sí que importa, és ruta o paràmetre de consulta.

Consell: imbrica el mapa igual que imbricaries els components. Si en dibuixar la pantalla el marc conté la secció i aquesta la pestanya, el mapa ha de tenir aquesta mateixa forma. Quan el mapa i el disseny divergeixen, apareixen els remuntatges i les duplicacions.

Consell: usa handle per a tot el que és declaratiu de la ruta. Molles, títol del document, icona del menú, nivell de permís. És un camp lliure i converteix el mapa en l'única font de veritat sobre la navegació.

Exercicis

Exercici 1: la secció de reserves imbricada

Reorganitza la branca de reserves perquè /reservas i /reservas/nueva comparteixin un marc comú, MarcReserves, que mostri una capçalera «Reserves» i un petit menú amb dos enllaços relatius («Les meves reserves» i «Nova reserva») marcant l'actiu. Requisits:

  • Cap URL ha de canviar: continuen sent /reservas i /reservas/nueva.
  • MarcReserves no s'ha de tornar a muntar en passar d'una a l'altra.
  • L'enllaç «Les meves reserves» no ha d'aparèixer actiu estant a /reservas/nueva.

Exercici 2: títol del document des del mapa

Fent servir handle i useMatches, escriu un hook useTitolDeRuta que posi a document.title un text compost per la molla de la ruta més profunda i el nom de l'aplicació, per exemple Parc Nord · CicloUrbano. S'ha d'actualitzar a cada navegació i usar el valor per defecte CicloUrbano quan cap ruta declari molla.

Exercici 3: contenir un fallat en una branca

La pestanya d'incidències té un fallat: si una bicicleta no té estat, bici.estat.toUpperCase() llança. Configura el mapa perquè aquest fallat no tombi la pantalla de detall de l'estació —el títol, les places i les pestanyes han de continuar visibles— sinó només el contingut de la pestanya. Explica quines rutes necessiten errorElement i per què no n'hi ha prou amb el de la ruta arrel.

Solucions

Solució 1

// src/rutes.jsx — fragment de la branca de reserves
{
  path: 'reservas',
  element: <MarcReserves />,          // ← ara sí que té element
  handle: { molla: 'Reserves' },
  children: [
    { index: true, element: <PaginaReserves /> },
    { path: 'nueva', element: <PaginaNovaReserva />, handle: { molla: 'Nova reserva' } }
  ]
}
// src/components/MarcReserves.jsx
import { NavLink, Outlet } from 'react-router';
import { useReserves } from '../contextos/ContextReserves.jsx';
import estils from './MarcReserves.module.css';

function MarcReserves() {
  const { estat } = useReserves();

  const classes = ({ isActive }) =>
    isActive ? `${estils.enllac} ${estils.actiu}` : estils.enllac;

  return (
    <section>
      <h2>Reserves</h2>
      <p>{estat.reserves.length} reserves registrades</p>

      <nav aria-label="Seccions de reserves">
        <NavLink to="." end className={classes}>
          Les meves reserves
        </NavLink>
        <NavLink to="nueva" className={classes}>
          Nova reserva
        </NavLink>
      </nav>

      <Outlet />
    </section>
  );
}

export default MarcReserves;

Les tres claus: les filles conserven index: true i path: 'nueva', així que les URL no canvien; MarcReserves és l'element del pare i per tant persisteix en navegar entre filles, canviant només l'<Outlet />; i l'end a to="." evita que «Les meves reserves» aparegui actiu a /reservas/nueva, perquè sense ell la seva destinació seria prefix de la de la germana.

Solució 2

// src/hooks/useTitolDeRuta.js
import { useEffect } from 'react';
import { useMatches } from 'react-router';

const NOM_APP = 'CicloUrbano';

/**
 * Sincronitza document.title amb la molla de la ruta activa més profunda.
 * Sense paràmetres. No retorna res.
 */
export function useTitolDeRuta() {
  const coincidencies = useMatches();

  const ambMolla = coincidencies.filter((c) => Boolean(c.handle?.molla));
  const ultima = ambMolla.at(-1);

  const text =
    typeof ultima?.handle.molla === 'function'
      ? ultima.handle.molla(ultima.params)
      : ultima?.handle.molla;

  useEffect(() => {
    // Sincronització amb un sistema extern (el document): cas de llibre de useEffect (05-02)
    document.title = text ? `${text} · ${NOM_APP}` : NOM_APP;
  }, [text]);
}

Es crida una sola vegada, des de Disseny, i funciona a totes les pantalles:

function Disseny() {
  useTitolDeRuta();
  // …
}

Dos comentaris. Primer, la dependència de l'efecte és text, una cadena, no l'array coincidencies: aquest últim és un objecte nou a cada render i provocaria que l'efecte s'executés sempre, el problema de dependències de 05-02. Segon, això és exactament la definició d'efecte que vam fixar en aquella lliçó: sincronitzar React amb un sistema extern, en aquest cas el títol del document.

Solució 3

Cal un errorElement a les rutes de les pestanyes, no només a l'arrel:

{
  path: ':estacionId',
  element: <PaginaDetallEstacio />,
  children: [
    { index: true, element: <PestanyaFlota />, errorElement: <ErrorPestanya /> },
    { path: 'incidencias', element: <PestanyaIncidencies />, errorElement: <ErrorPestanya /> }
  ]
}
// src/pagines/ErrorPestanya.jsx
import { useRouteError } from 'react-router';
import Avis from '../components/Avis.jsx';
import { registrarError } from '../utilitats/monitoritzacio.js';

function ErrorPestanya() {
  const error = useRouteError();
  registrarError(error, 'pestanya de detall d\'estació');

  return (
    <Avis to="error" titol="No hem pogut mostrar aquesta pestanya">
      <p>Les dades de l'estació continuen disponibles a l'altra pestanya.</p>
    </Avis>
  );
}

export default ErrorPestanya;

Per què no n'hi ha prou amb l'errorElement de l'arrel: React Router busca l'errorElement de la ruta que ha fallat i, si no en té, puja pels seus ancestres fins a trobar-ne un, substituint l'element d'aquesta ruta. Si el més proper és el de l'arrel, el que se substitueix és <Disseny /> sencer: desapareixen capçalera, molles, peu, títol de l'estació i pestanyes, i l'usuari es queda mirant una pantalla d'error completa per un fallat que afecta un requadre. Posant l'errorElement a la mateixa pestanya, la substitució passa exactament on hi havia l'<Outlet /> de PaginaDetallEstacio: la resta de la pantalla continua intacta i l'usuari pot canviar a l'altra pestanya.

És la mateixa lògica de granularitat de 04-05 amb els LimitError al voltant del catàleg i del formulari, ara expressada al mapa de rutes en lloc de al JSX. I ve amb un extra: en navegar a una altra ruta, l'estat d'error es neteja sol.

Conclusió

Les rutes imbricades són composició d'interfície expressada a través de la URL: una ruta pare declara children i obre un forat amb <Outlet />, i la filla que coincideix amb la URL es pinta allà. Has convertit Disseny en la ruta arrel de CicloUrbano, i el guany és concret i mesurable: Capcalera i PeuDePagina deixen de desmuntar-se i tornar-se a muntar a cada navegació, els seus efectes no es repeteixen, el seu estat intern persisteix, i els proveïdors que hi viuen —ProveidorReserves i ProveidorAvisos— sobreviuen als canvis de pantalla, que és el que permetrà crear una reserva en una pantalla i veure-la en una altra. Saps que index: true marca la pantalla per defecte d'una secció, en què es diferencia de path: '', i que una ruta sense path agrupa diverses pantalles sota un mateix marc sense afegir cap segment a la URL, patró del qual 06-05 en farà la seva eina principal.

Has construït el detall d'estació amb pestanyes imbricades: flota com a ruta índex i incidencias com a germana, amb NavLink relatius —to="." amb end i to="incidencias"— que marquen la pestanya activa sense interpolar l'identificador, de manera que cada pestanya té la seva pròpia adreça compartible i el botó enrere alterna entre elles. useOutletContext t'ha permès calcular l'estació i la seva flota una sola vegada al pare i baixar-les a les pestanyes, reservant el context de React per al que és veritablement ambiental. useMatches juntament amb el camp lliure handle ha convertit el mapa de rutes en la font de les molles de pa i del títol del document, la primera mostra clara de per què en el mode de dades convé que el mapa sigui una dada i no marcatge. I errorElement amb useRouteError et dona contenció de fallats per branca, complementària al LimitError de 04-05: aquell és la xarxa de seguretat global i l'única que existeix fora de l'enrutador; aquest captura a més els errors de loader/action, distingeix les Response llançades a propòsit amb isRouteErrorResponse i —avantatge gens menor— es neteja sol en navegar.

Tota la navegació que has escrit fins aquí neix d'un clic en un enllaç. Però hi ha situacions en què l'aplicació ha de navegar pel seu compte: quan l'usuari confirma el FormulariReserva a /reservas/nueva, el correcte és crear la reserva, despatxar-la al reductorReserves i portar-lo a /reservas sense que el formulari quedi a l'historial; quan algú intenta abandonar un formulari a mitges, convé aturar-lo i preguntar; i mentre una navegació és en curs, convé dir-l'hi. La propera lliçó és Navegació Programàtica.

Curs de React

Mòdul 1: Introducció a React

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats