Was ist Remix?
Remix ist ein React framework, das auf Webstandards aufbaut. Während viele Frameworks die Plattform abstrahieren, setzt Remix voll darauf: HTTP requests und responses, HTML forms, links, header und cookies bilden das Fundament, und das Framework erweitert diese, anstatt sie zu ersetzen.
Das Ergebnis ist eine Anwendung, die bereits funktioniert, bevor JavaScript geladen ist. Formulare werden abgeschickt, links navigieren und Daten erscheinen, weil der Server die Arbeit erledigt. Sobald JavaScript geladen ist, werden dieselben Interaktionen augenblicklich und partiell. Diese Eigenschaft nennt man progressive enhancement, und sie ist die zentrale Idee von Remix.
Im Jahr 2024 hat das Remix-Team das Framework in React Router v7 integriert, wo es nun als Framework-Modus existiert. Die Konzepte und APIs sind unverändert, sodass alle hier beschriebenen Informationen für beides gelten.
Verschachtelte Routen
Routen sind Dateien unter app/routes, wobei Punkte in Dateinamen den URL-Segmenten entsprechen.
app/routes/
├── _index.tsx # /
├── posts._index.tsx # /posts
├── posts.$slug.tsx # /posts/:slug
├── posts.$slug.edit.tsx # /posts/:slug/edit
└── api.health.tsx # /api/health
Routen werden verschachtelt, um der UI zu entsprechen. Eine Eltern-Route rendert ein gemeinsames Layout und ein <Outlet />, und die Kind-Routen werden darin gerendert. Da Remix den gesamten Routen-Baum kennt, kann es die Daten für eine Seite und alle ihre verschachtelten Routen parallel laden, anstatt in einem Waterfall.
// app/routes/posts.tsx
import { Outlet } from "@remix-run/react";
export default function PostsLayout() {
return (
<div className="posts">
<h1>Posts</h1>
<Outlet />
</div>
);
}
Loader
Ein loader ist eine Server-Funktion, die einer Route Daten bereitstellt, bevor diese gerendert wird. Da er auf dem Server ausgeführt wird, kann er direkt mit einer Datenbank kommunizieren.
// app/routes/posts.$slug.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug);
if (!post) throw json({ message: "Post not found" }, { status: 404 });
return json({ post });
}
export default function Post() {
const { post } = useLoaderData<typeof loader>();
return <article>{post.title}</article>;
}
Es muss kein Ladezustand verwaltet werden und es gibt keine clientseitigen Requests. Remix ruft den loader auf dem Server für das initiale Rendering sowie bei der Navigation auf, und die Komponente erhält die Daten synchron.
Actions und Formulare
Mutationen werden durch actions gehandhabt. Dabei handelt es sich um Server-Funktionen, die an eine Route gebunden sind und durch das Absenden von Formularen ausgelöst werden.
// app/routes/posts.new.tsx
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const data = await request.formData();
const title = String(data.get("title") ?? "").trim();
if (!title) return json({ error: "Title is required" }, { status: 400 });
await createPost(title);
return redirect("/posts");
}
export default function NewPost() {
return (
<Form method="post">
<input name="title" required />
<button type="submit">Create</button>
</Form>
);
}
Da es sich hierbei um ein echtes Formular handelt, funktioniert es auch ohne JavaScript. Remix fängt das Absenden ab, sobald der Client bereit ist, und validiert die betroffenen loader automatisch neu. So spiegelt die UI die neuen Daten wider, ohne dass manuelle Cache-Updates erforderlich sind.
Error Boundaries
Jede Route kann eine ErrorBoundary exportieren, die Fehler aus ihrem Loader, ihrer Action oder ihrer Komponente abfängt.
// app/routes/posts.$slug.tsx
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <h1>Post not found</h1>;
}
return <h1>Something went wrong</h1>;
}
Boundaries sind verschachtelt, sodass ein Fehler in einer Child-Route nicht die gesamte Seite leert. Das umschließende Layout bleibt funktionsfähig, was eine wesentlich bessere User Experience ist als ein weißer Bildschirm.
Ressourcen-Routes und Webstandards
Eine Route ohne Default-Export ist eine Ressourcen-Route: Sie gibt Daten anstelle einer UI zurück, was ideal für APIs und Webhooks ist.
// app/routes/api.health.tsx
export async function loader() {
return json({ status: "ok", time: Date.now() });
}
Da Remix die Request und Response der Plattform nutzt, können Sie Header auslesen, Cookies setzen, Antworten streamen und beliebige Statuscodes zurückgeben, ohne eine framework-spezifische API erlernen zu müssen. Dieses Wissen lässt sich direkt auf andere Runtimes übertragen.
Warum sich das Modell anders anfühlt
Die meisten React-Apps verwalten Daten in Effects und speichern sie im Client-State, um diesen Zustand anschließend über API-Routes wieder mit dem Server zu synchronisieren. Remix bricht diese Ebenen auf:
- Die Route besitzt ihre Daten über einen loader.
- Die Route besitzt ihre Mutationen über eine action.
- Der Server besitzt die “Source of Truth”, während der Client schlank bleibt.
Weniger bewegliche Teile bedeuten weniger Bugs, weniger zu synchronisierender State und weniger zu schreibender Code. Der Kompromiss ist eine striktere Meinung darüber, wie Daten fließen – und genau das ist der Grund, warum Teams, die Remix einsetzen, es meist sehr schätzen.
Best Practices
- Daten in Loadern abrufen, nicht in Effects.
- Mutationen über Actions und Forms durchführen und die Revalidierung durch Remix übernehmen lassen.
FormundLinkvon Remix verwenden, damit das Enhancement automatisch funktioniert.- Auf Routen, die fehlschlagen können, eine
ErrorBoundaryexportieren. - Verschachtelte Routen an der UI-Hierarchie ausrichten.
- Resource Routes für APIs und Webhooks nutzen.
- Korrekte Statuscodes und Header aus Loadern und Actions zurückgeben.
Häufige Fehler
- Fetching in
useEffectanstatt in einem loader, wodurch das Server-Rendering verloren geht. - Verwendung von
fetchfür Mutationen anstatt einer action und die manuelle Revalidierung der Daten. - Das Vergessen von
<Outlet />in einer übergeordneten Layout-Route. - Das Verschlucken von Fehlern, anstatt sie von Boundaries handhaben zu lassen.
- Over-fetching in einem Parent-Loader, obwohl eine Child-Route die Daten besitzen sollte.
- Remix wie eine clientseitige SPA zu behandeln und gegen dessen Server-Modell zu arbeiten.
Wie geht es weiter?
Remix ist die konsequenteste Umsetzung des „use the platform“-Ansatzes in der React-Welt. Vergleiche es mit Next.js, um den anderen großen React-Ansatz kennenzulernen, und mit SvelteKit für eine ähnliche Philosophie in einem anderen Ökosystem. Vertiefe deine Grundlagen in React, TypeScript und Node.js und baue anschließend eine kleine App mit einem loader, einer action und einer error boundary.