12k
All articles

So funktioniert das Laden von Daten in Remix 3

Erfahren Sie, wie Remix 3 Loader durch Controller-Actions zum Laden von Daten ersetzt und wie Routen, Streaming-Frames, Wartezustände und Fehler funktionieren.

OpenReplay Team
OpenReplay Team
So funktioniert das Laden von Daten in Remix 3

Remix 3 lädt Seitendaten in einer Controller-Action, die eine GET-Route verarbeitet. Diese Action gibt eine standardmäßige Web-Response zurück, die in der Regel von context.render() erzeugt wird.

Wenn Sie bereits eine Remix-2-App ausgeliefert haben, suchen Sie vermutlich zuerst nach dem loader-Export und dem Hook, der ihn ausliest. In den neuen Guides finden Sie beides nicht. Ältere Beiträge, die behaupten, Loader seien in Remix 3 übernommen worden, stimmen nicht mit der Remix-3-Dokumentation überein.

Remix 3 (v3.0.0), veröffentlicht am 1. Oktober 2026, verwendet ein anderes Modell als den Loader-Ansatz aus Remix 1/2, der in diesem Überblick über die ursprüngliche, Loader-basierte Architektur von Remix beschrieben wird. Dieser Artikel erklärt, wo das Data Fetching jetzt stattfindet, wie Erstaufrufe und Navigationen gestreamt werden, wie Pending-UI funktioniert und wohin Fehler gehen.

Die wichtigsten Erkenntnisse

  • Remix 3 lädt Seitendaten in einer createController()-Action für eine GET-Route. Die Action übergibt diese Daten als Props an die Komponente, die sie mit context.render() rendert.
  • Routen werden in app/routes.ts deklariert, durch Controller implementiert und mit router.map() registriert. Jeder Controller ist nur für die direkten Blätter seiner Route Map zuständig.
  • Ein Frame ohne fallback hält das initiale HTML zurück, bis seine Route antwortet. Ein Frame mit fallback streamt zuerst diesen Fallback und ersetzt ihn später.
  • Erwartbare Fehlerfälle wie fehlende Datensätze sollten als Response-Objekte mit Status 404 oder 400 zurückgegeben werden. Ein geworfener Fehler führt dazu, dass router.fetch() rejected, und die Server-Boundary gibt einen 500er zurück.
  • Schlägt ein verzögerter Frame fehl, nachdem sein Fallback bereits gesendet wurde, wird der Fehler gemeldet und der Fallback bleibt auf dem Bildschirm.

Was hat den Loader in Remix 3 ersetzt?

In Remix 3 übernimmt eine Controller-Action die Aufgabe, die früher ein Route-Loader erfüllt hat. Die Route Map deklariert eine URL und eine HTTP-Methode, und createController() bindet jedem Blatt der Route genau eine Action zu. Die Action liest typisierte Werte aus context.params, wartet per await auf Ihre Daten und gibt eine Response zurück. Der Routing and Controllers Guide zeigt dieses Muster, und Loader-Exporte kommen darin nirgends vor.

// app/actions/albums/controller.tsx
import { createController } from "remix/router";

import { routes } from "../../routes.ts";
// getAlbum and getRecommendations are app data functions, not Remix APIs.
import { getAlbum, getRecommendations } from "./data.ts";
import { AlbumRecommendations } from "./recommendations.tsx";
import { AlbumPage } from "./show-page.tsx";

export default createController(routes.albums, {
  actions: {
    async show(context) {
      // The second argument is an app-defined signal parameter.
      let album = await getAlbum(context.params.albumId, context.request.signal);
      if (!album) return new Response("Album not found", { status: 404 });

      return context.render(<AlbumPage album={album} />);
    },

    async recommendations(context) {
      let album = await getAlbum(context.params.albumId, context.request.signal);
      if (!album) return new Response("Album not found", { status: 404 });

      let recommendations = await getRecommendations(album);
      return context.render(<AlbumRecommendations albums={recommendations} />);
    },
  },
});

Die wesentliche Änderung gegenüber Remix 2 ist die Richtung des Datenflusses. In Remix 2 hat sich die Komponente ihre Daten über einen Hook geholt (Pull). In Remix 3 übergibt die Action die Daten als Props an die Komponente (Push). context.render existiert nur, wenn die render()-Middleware im Router installiert ist. Ohne sie kann eine Action trotzdem eine einfache Response oder Response.json() zurückgeben.

Wo findet das Laden von Daten in Remix 3 statt?

Eine Remix-3-App deklariert ihre URLs und HTTP-Methoden in app/routes.ts, implementiert mit createController() eine Action pro Routenblatt und registriert jeden Controller mit router.map().

// app/routes.ts
import { get, route } from "remix/routes";

export const routes = route({
  albums: route("/albums", {
    show: get("/:albumId"),
    recommendations: get("/:albumId/recommendations"),
  }),
});

routes.albums.show.href({ albumId: "thriller" }); // "/albums/thriller"

Der href()-Helper jeder Route ist typisiert. Da :albumId eine Pfadvariable ist, ist albumId ein Pflichtargument. Wenn Sie das Pattern später umbenennen, schlägt die Typprüfung bei jedem Aufruf fehl, der noch den alten Schlüssel übergibt.

// app/router.ts
import { createRouter } from "remix/router";

import albumsController from "./actions/albums/controller.tsx";
import { render } from "./middleware/render.tsx"; // the app's own render middleware, built with renderWith()
import { routes } from "./routes.ts";

export const router = createRouter({
  middleware: [render()],
});

router.map(routes.albums, albumsController);

Ein Controller ist nur für die direkten Blätter der übergebenen Map zuständig, daher benötigen verschachtelte Maps einen eigenen router.map()-Aufruf. Remix prüft jedes Mapping beim Setup: Fehlt einem Controller die Action für eines seiner Blätter, wirft das Setup einen Fehler, bevor überhaupt ein Request verarbeitet wird. In der Praxis sollten Sie eine GET-Route pro Seite einplanen, plus eine weitere für jeden Bereich, der eigenständig geladen wird.

Wie funktionieren Erstaufruf und Navigation in Remix 3?

Beim Erstaufruf streamt die Render-Middleware von Remix 3 ein einzelnes HTML-Dokument. Bei der Navigation können Same-Origin-Links und -Formulare, sobald run() im Browser gestartet ist, einen einzelnen benannten Frame neu befüllen, statt das gesamte Dokument zu ersetzen. Benötigt ein Bereich einen eigenen Request, rendert die Seite ihn mit Frame, der auf die GET-Route dieses Bereichs verweist. Die Action hinter dieser Route gibt nur das Markup des Bereichs zurück und lässt die Dokument-Shell der App weg.

// app/actions/albums/show-page.tsx
import { Frame } from "remix/component";
import type { Handle } from "remix/component";

import { routes } from "../../routes.ts";
import { Document } from "../document.tsx"; // app-defined shell component
import type { Album } from "./data.ts";

export function AlbumPage(handle: Handle<{ album: Album }>) {
  return () => {
    let { album } = handle.props;
    return (
      <Document title={album.title}>
        <h1>{album.title}</h1>
        <Frame
          name="album-recommendations"
          src={routes.albums.recommendations.href({ albumId: album.id })}
          fallback={<p>Loading recommendations…</p>}
        />
      </Document>
    );
  };
}

Ein Frame ohne fallback ist blockierend: Remix sendet den ersten HTML-Chunk erst, wenn die Route des Frames geantwortet hat, sodass die Seite vollständig ankommt. Fügen Sie einen fallback hinzu, wird dieser stattdessen im initialen HTML ausgeliefert und anschließend durch die Ausgabe der Frame-Route ersetzt, sobald deren Action abgeschlossen ist. Wählen Sie die blockierende Variante, wenn die Seite ohne den Bereich keinen Sinn ergibt, und einen Fallback, wenn es sich lohnt, den Rest der Seite schon vorher anzuzeigen.

Sobald run() im Browser gestartet ist, können Same-Origin-Links und -Formulare einen benannten Frame befüllen, statt durch das gesamte Dokument zu navigieren. Diese Attribute sind in Streaming UI with Frames dokumentiert:

AttributWirkung
data-rmx-targetWählt einen gemounteten, benannten Frame aus
data-rmx-srcLegt die Request-URL fest, mit der der Frame befüllt wird
data-rmx-reset-scroll="false"Behält die aktuelle Scrollposition bei
data-rmx-history="push|replace"Überschreibt das Standardverhalten der History
data-rmx-documentNimmt den Link bzw. das Formular von der Interception aus

Bei einem Link wechselt die Adressleiste weiterhin zu href, während data-rmx-src bestimmt, was in den Frame geladen wird.

<form
  action={routes.albums.recommendations.href({ albumId: album.id })}
  method="get"
  data-rmx-target="album-recommendations"
>
  <button type="submit">Refresh recommendations</button>
</form>

Komponenten können Frames auch selbst neu laden. handle.frame.reload() lädt den Frame neu, der die Komponente enthält, und await handle.frames.get("album-recommendations")?.reload() zielt auf einen benannten Frame an anderer Stelle der Seite. Ist kein Frame mit diesem Namen gemountet, liefert handle.frames.get() undefined zurück. Frame-Routen sollten ausschließlich vertrauenswürdiges HTML zurückgeben; bereinigen Sie nicht vertrauenswürdige Inhalte daher vorher auf dem Server.

Pending-States: Fallbacks und Abbruch

Remix 3 realisiert Pending-UI über Frame-Fallbacks und den Abbruch von Requests. Der Frame-Fallback ist die initiale Lade-UI; einen globalen Hook für den Pending-Status der Navigation dokumentieren die Guides nicht.

Den Abbruch muss die App selbst verdrahten. Der Browser-Frame-Resolver, den Sie an run() übergeben, erhält ein Signal, und laut den Guides sollten Sie es an fetch() weiterreichen, damit ein Frame-Request abbricht, wenn sein Frame entfernt oder durch einen neueren Request ersetzt wird. Auf dem Server übergibt die Render-Middleware request.signal an renderToStream() und an ihre Frame-Requests. Damit der Abbruch auch Ihren eigenen Code erreicht, übergeben Sie request.signal ebenfalls an Datenbank- und Fetch-Aufrufe. Laut Errors and Cancellation rejected der Router bei einem abgebrochenen Request mit request.signal.reason.

Fehlerzustände: Responses und Reporting-Hooks

Geben Sie in Remix 3 einen fehlenden Datensatz oder ungültige Eingaben als Response mit Status 404 oder 400 zurück. Ein geworfener Fehler führt dazu, dass router.fetch() rejected, und die Server-Boundary antwortet dann mit einem 500er. Remix 3 hat keine allumfassende Error Boundary auf Komponentenebene; jede Art von Fehler hat ihr eigenes Ziel:

FehlerWohin er geht
Erwartbar (fehlender Datensatz, ungültige Eingabe)Zurückgegebene Response mit Status 404 oder 400
Throw in einer Action oder Middlewarerouter.fetch() rejected; die Server-Boundary meldet den Fehler und gibt einen 500er zurück
Render-Fehler im Root oder in einem blockierenden FrameDer Fehler wird gemeldet und der Stream rejected
Fehler in einem verzögerten Frame, nachdem der Fallback gesendet wurdeDer Fehler wird gemeldet; der Fallback bleibt auf der Seite
Browser-Runtime (Hydration, Komponente, Scheduler, Frame)error-Event auf dem von run() zurückgegebenen Wert

An der Server-Boundary akzeptiert createRequestListener aus remix/node-fetch-server eine onError-Option. Dort werden nicht abgefangene Fehler geloggt, und optional kann sie eine Response zurückgeben, die anstelle des Standard-500ers gesendet wird. Die onError-Option der Render-Middleware und das error-Event im Browser melden Fehler lediglich. Bei Frames entscheidet der Resolver Ihrer App, was aus einer fehlgeschlagenen Response wird: abgegrenztes HTML für den jeweiligen Bereich oder ein Fehler, der an diese Reporting-Hooks geht. Der Resolver aus den Guides wirft bei jedem Status, der nicht OK ist, sodass jeder fehlgeschlagene Frame-Request gemeldet wird.

Schlägt ein verzögerter Frame fehl, nachdem sein Fallback bereits gesendet wurde, kann der Server die Seite nicht mehr durch eine Fehler-Response ersetzen, weil bereits Body-Bytes übertragen wurden. Der Fehler wird gemeldet und der Fallback bleibt auf dem Bildschirm. Ein Server-Log hält diesen einen Fehler fest, zeigt aber nicht, was der Nutzer gesehen hat. Session Replay schließt diese Lücke: Sie können beobachten, wie Nutzer vor einer „Loading…“-Meldung sitzen, die nie aufgelöst wurde, während sie den Rest der Seite weiter nutzen. Formulieren Sie Fallbacks so, dass ein hängengebliebener Bereich auch dann noch sinnvoll ist, wenn er nie befüllt wird.

Migration von Remix 2

Der Umstieg einer Remix-2-App auf Remix 3 ist eine Neuentwicklung, kein Upgrade. Routen wandern in eine typisierte Route Map, das Laden von Daten in Controller-Actions und die UI auf die eigene Komponenten-Runtime von Remix in remix/component. Die Guides enthalten kein Migrationskapitel. Der In-Place-Upgrade-Pfad für Remix 2 ist React Router v7, und Remix 3 ist eine separate Codebasis. Betrachten Sie Anleitungen wie dieses Next.js-zu-Remix-Walkthrough, geschrieben für Remix 1/2, nur als Hintergrundwissen, nicht als Leitfaden für Remix 3.

Fazit

Das Laden von Daten in Remix 3 lässt sich auf Folgendes reduzieren: eine GET-Route, eine Controller-Action, die auf Daten wartet und eine Response zurückgibt, sowie Frames für Bereiche, die eigenständig laden oder fehlschlagen. Für den Einstieg mappen Sie die Routen einer Seite in app/routes.ts, verlagern deren Data Fetching in einen Controller und entscheiden, welche Bereiche einen Fallback benötigen. Mehr zur Arbeit mit dem neuen Framework erfahren Sie im Artikel darüber, wie Remix 3 für KI-Coding-Agents konzipiert ist.

FAQs

Verwendet Remix 3 React?

Nein. Remix 3 behält die JSX-Syntax bei, läuft aber weder auf React noch auf React Router. Es nutzt ein eigenes Komponentenmodell, das aus remix/component exportiert wird. Eine Komponente besteht aus einer äußeren Setup-Funktion, die ein Handle erhält und einmal ausgeführt wird, sowie einer inneren Render-Funktion, die bei jedem Update ausgeführt wird. React-Hooks wie useState sind nicht Teil dieses Modells, daher funktionieren React-Komponentenbibliotheken in Remix 3 nicht ohne Anpassungen.

Wie funktionieren Formular-Submits und Mutationen in Remix 3?

Deklarieren Sie das Formular mit dem Route-Helper form() in app/routes.ts. Standardmäßig erzeugt er zwei Blätter unter derselben URL: eine index-GET-Route, die das Formular anzeigt, und eine action-POST-Route, die den Submit verarbeitet. Beide Blätter gehören zum selben Controller. Nach einem erfolgreichen Create, Update oder Delete empfehlen die Remix-3-Guides, einen 303-Redirect zurückzugeben, damit ein Neuladen der Seite das Formular nicht erneut absendet.

Kann eine Controller-Action in Remix 3 JSON statt HTML zurückgeben?

Ja. Eine Controller-Action kann jede beliebige Web-Response zurückgeben, einschließlich Response.json(), Redirects, Dateien und Streams. Der Remix-3-Guide Forms and Mutations empfiehlt JSON für Fälle wie Autovervollständigung, Polling, in sich geschlossene Widgets, APIs und Aufrufe zwischen Services. Er rät davon ab, für ein Formular, das bereits über eine HTML-Response und einen Redirect funktioniert, einen separaten JSON-Mutationspfad hinzuzufügen.

Ist Remix 3 dasselbe wie React Router v7?

Nein. Remix v2 wurde in React Router v7, veröffentlicht im November 2024, als dessen Framework Mode integriert. Das ist der Upgrade-Pfad für Remix-2-Apps, die Loader und React beibehalten möchten. Remix 3 ist ein separates Projekt, das im Mai 2025 angekündigt wurde und auf einem eigenen Komponentenmodell basiert. Der Umstieg darauf bedeutet, Routen, das Laden von Daten und die UI neu zu schreiben.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.