# React Router Data Router: Die URL als Zustandsort

URL: https://www.mikebild.dev/de/blog/react-router-data-router/

Eine Route ist im Data Router von React Router keine Zuordnung von Pfad zu Komponente mehr. Sie ist eine vollständige Beschreibung dessen, was zu einer Adresse gehört: welcher Ausschnitt der Oberfläche erscheint, woher seine Daten kommen, was bei einem Schreibvorgang passiert, was im Fehlerfall zu sehen ist. Diese Beschreibungen sind Objekte, und weil sie schon vor dem ersten Rendern vorliegen, kann der Router aus ihnen ablesen, was für eine URL zu tun ist – ohne eine einzige Komponente laufen zu lassen.

Wie weit das trägt, zeigt der Vergleich mit dem Muster, das die klassische Variante nahelegt: Eine Route rendert eine Komponente, die Komponente hat einen `useEffect`, der Effekt lädt die Daten, und daneben liegen drei `useState` für Ladezustand, Fehler und Ergebnis. Bei einer Seite geht das. Bei zwanzig verschachtelten Seiten entstehen Ladewasserfälle: Die äußere Komponente rendert, ihr Effekt lädt, erst danach rendert die innere, ihr Effekt lädt – nacheinander, obwohl beide von Anfang an bekannt waren. Im React-Überblick habe ich Routing noch mit `BrowserRouter` und `Routes` gezeigt und die moderne Variante bewusst liegengelassen; sie ist der Gegenstand dieses Beitrags. Der Data Router dreht die Reihenfolge um.

**Redaktionelle Einordnung (Juli 2026):** React Router 8 unterscheidet drei Modi: Declarative, Data und Framework. Dieser Beitrag behandelt den Data Mode mit `createBrowserRouter`. Der Framework Mode baut darauf auf und ergänzt unter anderem typisierte Route-Module, Code-Splitting sowie mögliche SPA-, SSR- und statische Rendering-Strategien.

## Die Kernidee: Die Route lädt, bevor sie rendert

Der entscheidende Unterschied lässt sich in einem Satz sagen. Bei der alten Variante rendert die Komponente zuerst und lädt dann ihre Daten. Beim Data Router lädt die Route zuerst ihre Daten und rendert dann die Komponente – mit den Daten bereits in der Hand. Jede Route bekommt dafür eine Funktion, den `loader`. Er läuft beim Navigieren, noch bevor irgendetwas auf den Bildschirm kommt. Erst wenn er fertig ist, rendert React die Komponente, und die greift die Daten fertig geladen ab, statt sie selbst zu besorgen.

Damit verschwindet die Ladelogik aus dem Komponenten-Body. Kein `useEffect` mehr, der beim ersten Rendern nachlädt, keine Handvoll `useState` für einen Zustand, den es genau genommen nie geben sollte – denn wenn die Komponente rendert, sind die Daten längst da. Das schreibende Gegenstück heißt `action`. Es nimmt Formular-Übermittlungen entgegen, schreibt zum Server, und danach lädt React Router die betroffenen `loader` von selbst neu, damit die Ansicht wieder stimmt.

## `createBrowserRouter` und die Route-Objekte

Man beschreibt die Anwendung nicht mehr als verschachtelte JSX-Elemente, sondern als Objekte. `createBrowserRouter` nimmt ein Array von Route-Definitionen entgegen, und jede Definition trägt ihre Bausteine an einer Stelle: den Pfad, die Komponente, den `loader`, die `action`, das Fehler-Element.

```jsx
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";

const router = createBrowserRouter([
  {
    path: "/invoices",
    element: <InvoiceList />,
    loader: invoiceListLoader,
    errorElement: <RouteError />,
    children: [
      {
        path: ":invoiceId",
        element: <InvoiceDetail />,
        loader: invoiceDetailLoader,
        action: invoiceDetailAction,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}
```

Statt `path` und `element` über die halbe Anwendung verstreut zu suchen, steht alles, was eine Route ausmacht, beisammen: was sie anzeigt, woher ihre Daten kommen, wie sie auf Schreibvorgänge reagiert, was bei einem Fehler passiert. Diese Bündelung ist kein kosmetischer Vorteil. Sie ist die Voraussetzung dafür, dass React Router die Daten schon kennt, bevor gerendert wird – denn der Router kann aus dem Objektbaum ablesen, welche `loader` für eine URL zuständig sind, ohne die Komponenten erst zu rendern.

## Der `loader`: Daten holen, bevor gerendert wird

Ein `loader` ist eine gewöhnliche Funktion. Sie bekommt unter anderem die Parameter aus dem Pfad, holt die Daten und gibt sie zurück. In der Komponente greift man sie mit `useLoaderData()` ab.

```jsx
import { useLoaderData } from "react-router";

async function invoiceDetailLoader({ params }) {
  const response = await fetch(`/api/invoices/${params.invoiceId}`);
  if (!response.ok) {
    throw new Response("Not found", { status: 404 });
  }
  return response.json();
}

function InvoiceDetail() {
  const invoice = useLoaderData();
  return <h1>Invoice {invoice.number}</h1>;
}
```

Man beachte, was hier nicht steht. Kein `useState` für die Rechnung, kein `useEffect` fürs Laden, keine Prüfung auf `null`, solange noch geladen wird. Wenn `InvoiceDetail` rendert, ist der `loader` abgeschlossen. Ob `invoice` fachlich existiert, muss der Loader selbst absichern – etwa indem er bei einem leeren Ergebnis eine 404-Response wirft. Das Warten des Routers ersetzt diese Validierung nicht.

Der eigentliche Gewinn zeigt sich bei verschachtelten Routen. Öffnet jemand `/invoices/42`, sind zwei Routen betroffen: die Liste und das Detail. Bei der `useEffect`-Variante liefe das nacheinander – erst rendert die Liste und lädt, dann rendert das Detail und lädt. React Router kennt beide `loader` von vornherein und startet sie **parallel**. Damit fällt der Wasserfall weg: Alle betroffenen `loader` laufen gleichzeitig los, und die Seite ist fertig, sobald der langsamste durch ist.

```mermaid
flowchart TB
  Nav["Navigation zu /invoices/42"]
  Nav --> Match["Router ermittelt betroffene Routen"]
  Match --> L1["loader Liste"]
  Match --> L2["loader Detail"]
  L1 --> Ready["alle loader fertig"]
  L2 --> Ready
  Ready --> Render["Komponenten rendern<br/>mit useLoaderData"]
```

Hier lohnt der Bezug zum Server-Cache. Im Beitrag zu [Client-Zustand und Server-Cache](https://www.mikebild.dev/de/blog/client-zustand-und-server-cache/) habe ich gezeigt, dass Serverdaten kein Zustand sind, den man besitzt, sondern eine Kopie, die man verwaltet – mit Schlüssel, Alter und Invalidierung, wofür sich eine Bibliothek wie TanStack Query anbietet. Der `loader` liegt eine Ebene darüber. Er sagt, *wann* geladen wird, nämlich beim Navigieren, gebunden an die Route statt an die Komponente. Ob dahinter ein nackter `fetch` steht oder ein Cache, ist davon unberührt. Beides ergänzt sich sauber: Der `loader` ruft den `queryClient` auf, und die Bibliothek entscheidet, ob sie eine frische Kopie hält oder wirklich neu lädt. Der Router bestimmt den Zeitpunkt, der Cache die Gültigkeit – sie schließen sich nicht aus, sie greifen ineinander.

## Die `action`: Schreiben über `<Form>`

Für schreibende Vorgänge hält React Router eine eigene Komponente bereit. `<Form method="post">` sieht aus wie ein gewöhnliches HTML-Formular, wird aber vom Router abgefangen: Statt die Seite neu zu laden, ruft er die `action` der passenden Route auf und übergibt ihr die Formulardaten.

```jsx
import { Form, redirect } from "react-router";

async function invoiceDetailAction({ request, params }) {
  const formData = await request.formData();
  await fetch(`/api/invoices/${params.invoiceId}/pay`, {
    method: "POST",
    body: formData,
  });
  return redirect(`/invoices/${params.invoiceId}`);
}

function InvoiceDetail() {
  const invoice = useLoaderData();
  return (
    <Form method="post">
      <button type="submit">Mark as paid</button>
    </Form>
  );
}
```

Das Entscheidende passiert nach der `action`: React Router revalidiert standardmäßig die Loader der aktiven Routen. Die Rechnung wurde bezahlt, die `action` ist durch, und die Ansicht kann den frischen Stand zeigen, weil die betroffenen Loader noch einmal gelaufen sind. Mit `shouldRevalidate` lässt sich dieses Verhalten einschränken; externe Caches müssen weiterhin passend invalidiert werden. Der Ablauf ähnelt dem Browserprinzip „Formular abschicken, Seite neu holen“, nur ohne den vollen Dokument-Neuaufbau.

```mermaid
flowchart LR
  F["Form method=post"] --> A["action schreibt<br/>zum Server"]
  A --> R["loader werden<br/>revalidiert"]
  R --> U["Ansicht zeigt<br/>frischen Stand"]
```

Wer das mit dem `useEffect`-Dashboard aus dem Server-Cache-Beitrag vergleicht, erkennt dieselbe Krankheit und eine andere Medizin. Dort liefen die Zahl oben und die Liste daneben auseinander, weil niemand nach dem Bezahlen dafür sorgte, dass beide neu luden. Hier laufen sie nach der `action` gemeinsam neu, weil die Revalidierung alle `loader` der aktiven Route-Ebenen betrifft und nicht eine einzelne Komponente.

## Verschachtelte Routen und `<Outlet />`

Die Objekte aus dem ersten Beispiel hatten `children`, und das ist mehr als eine Verwaltungsfrage. Verschachtelte Routen bilden die UI-Hierarchie ab: Eine äußere Layout-Route rendert den Rahmen – Navigation, Kopfzeile, Randspalte –, und dort, wo die innere Route erscheinen soll, setzt sie einen `<Outlet />`.

```jsx
import { Outlet, NavLink } from "react-router";

function InvoiceList() {
  const invoices = useLoaderData();
  return (
    <div className="layout">
      <nav>
        {invoices.map((invoice) => (
          <NavLink key={invoice.id} to={`/invoices/${invoice.id}`}>
            {invoice.number}
          </NavLink>
        ))}
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}
```

Navigiert man im Data Mode von `/invoices/42` zu `/invoices/43`, bleibt die äußere Route ohne eigenen geänderten Parameter standardmäßig stehen; ihr Loader läuft in diesem Beispiel nicht erneut, und nur die innere Route wechselt ihre Daten und ihren Inhalt im `<Outlet />`. Geänderte Suchparameter oder eine eigene `shouldRevalidate`-Regel können eine breitere Revalidierung auslösen. Die URL liest sich damit wie ein Pfad durch den Baum der Oberfläche: Jedes Segment steht für eine Ebene, jede Ebene für eine Route, jede Route für ein Stück UI mit eigenen Daten. Eine Layout-Route ganz ohne `path` kann übrigens allein dem gemeinsamen Rahmen dienen, während ihre Kinder die eigentlichen Adressen tragen.

## Die URL als Zustand: Suchparameter

Bis hierher ging es um den Pfad. Der zweite Teil der URL ist mindestens so nützlich: die Suchparameter hinter dem `?`. Im React-Überblick habe ich dafür plädiert, dass Auswahl und Filter in die URL gehören – und `useSearchParams` ist das Werkzeug dazu. Es fühlt sich an wie `useState`, schreibt seinen Wert aber in die Adresszeile.

```jsx
import { useSearchParams } from "react-router";

function InvoiceFilter() {
  const [searchParams, setSearchParams] = useSearchParams();
  const status = searchParams.get("status") ?? "all";

  return (
    <select
      value={status}
      onChange={(event) =>
        setSearchParams({ status: event.target.value })
      }
    >
      <option value="all">All</option>
      <option value="open">Open</option>
      <option value="paid">Paid</option>
    </select>
  );
}
```

Der gewählte Filter steht jetzt in der URL – `/invoices?status=open` –, und das ändert seinen Charakter grundlegend. Dieser Zustand ist teilbar: Man schickt den Link, und der Empfänger sieht dieselbe gefilterte Liste. Er ist mit einem Lesezeichen speicherbar. Er überlebt einen Reload, weil er nicht im Komponentenspeicher liegt, sondern in der Adresse. Und er lässt sich mit dem `loader` verbinden: Da der `loader` Zugriff auf die `request`-URL hat, kann er den `status`-Parameter auslesen und gleich die passend gefilterte Liste vom Server holen. Der Filter steht in der URL, der `loader` liest ihn, der Server liefert – aus einem Stück UI-Zustand wird ein navigierbarer Ort.

## Fehler und langsame Teildaten

Weil der `loader` vor dem Rendern läuft, braucht das Fehlschlagen einen eigenen Platz. Wirft ein `loader` oder eine `action` – etwa die `throw new Response(..., { status: 404 })` von oben –, sucht React Router die nächste Route mit `errorElement` und rendert diese statt der Komponente. Innerhalb des Fehler-Elements kommt man mit `useRouteError()` an das, was geworfen wurde.

```jsx
import { useRouteError } from "react-router";

function RouteError() {
  const error = useRouteError();
  return <p role="alert">Something went wrong: {error.status}</p>;
}
```

Das ersetzt viele verstreute `if (error)`-Zweige durch eine Stelle auf Route-Ebene. Die Route Error Boundary behandelt nicht nur Fehler aus `loader` und `action`, sondern auch Renderfehler in ihrem Routenast. Erwartbare fachliche Fehler sollte die Anwendung trotzdem bewusst modellieren, statt jeden negativen Zustand als Exception zu behandeln.

Ein Randthema sei nur angerissen: Manchmal soll die Seite nicht auf die langsamste Datenquelle warten. React Router 8 hat die frühere `defer`-Hilfsfunktion entfernt. Heute gibt ein Loader nicht kritische Daten als noch nicht aufgelöstes Promise zurück; in der Komponente wartet `<Await>` innerhalb einer `<Suspense>`-Grenze darauf. Kritische Daten werden im Loader weiterhin `await`et. So blockiert ein träger Teil nicht die ganze Ansicht, ohne dass eine spezielle `defer`-Response nötig ist.

## Ein Wort zu den drei Modi

Was ich hier gezeigt habe, heißt in der aktuellen Dokumentation Data Mode: `createBrowserRouter` in einer bestehenden Anwendung. Der Declarative Mode mit `BrowserRouter` bietet das klassische Routing ohne die Data APIs. Der Framework Mode setzt auf den Data Mode auf, organisiert Routen als Module und ergänzt Build- und Server-Funktionen. Das ist ein eigenes, größeres Thema, und ich lasse es hier bewusst stehen. Die Konzepte, die zählen – `loader`, `action`, Revalidierung, die URL als Zustandsort –, bleiben im Data und Framework Mode dieselben.

## Wohin die Daten gehören

Der rote Faden dieses Beitrags ist eine einzige Verschiebung. Beim klassischen Ansatz hängen Laden und Schreiben an der Komponente: Sie rendert, ihr Effekt holt, ihre Handler schreiben, und bei Verschachtelung reihen sich die Ladevorgänge zu einem Wasserfall. Der Data Router verankert beides an der Route. Der `loader` holt die Daten, bevor gerendert wird, und benachbarte Ebenen parallel; die `action` schreibt und lässt die `loader` danach von selbst revalidieren. Was in der Komponente bleibt, ist das Anzeigen – nicht das Besorgen.

Damit wird aus der URL endgültig das, was sie im Überblick nur behauptet hat zu sein. Sie ist nicht mehr das Adressfeld, das anzeigt, wo man ist. Sie ist der Ort, an dem der navigierbare Zustand wirklich liegt: Der Pfad bestimmt, welche Routen und damit welche Daten geladen werden, die Suchparameter tragen Filter und Auswahl, und beides zusammen ist teilbar, speicherbar und übersteht jeden Reload. Wer einmal so baut, gibt die Ladelogik nicht ungern aus dem Komponenten-Body her.

## Weiterführende Quellen

- Repository introduction-react (Routing-Kapitel, Ordner recipes zum Data Fetching): https://github.com/mikebild/introduction-react
- React Router, offizielle Dokumentation: https://reactrouter.com/
- React Router, „Picking a Mode“: https://reactrouter.com/start/modes
- React Router, „Data Loading“: https://reactrouter.com/start/data/data-loading
- React Router, „Actions“: https://reactrouter.com/start/data/actions
