Waku: React Server Components ohne Next.js
Waku ist ein schlankes React-Framework für React Server Components ohne Next.js. Erstellen Sie eine Beispiel-App und erfahren Sie mehr über Routing, Rendering und Deployment.
Waku ist ein schlankes React-Framework auf Basis von Vite, das Server Components und Server Actions aus React 19 ausführt. Zu lernen gibt es im Wesentlichen zwei Dinge: das dateibasierte Routing in src/pages und einen getConfig-Export pro Seite, der zwischen statischem und dynamischem Rendering wählt.
Wer Server Components bisher nur im App Router von Next.js eingesetzt hat, kann kaum unterscheiden, was zu React gehört und was zu Next. Waku behält das React-Modell bei und verzichtet auf den Großteil des umgebenden Frameworks. Dieser Artikel baut eine kleine App vom Scaffolding bis zum Deployment auf. So können Sie entscheiden, ob sich ein Versuch mit Waku in einem Nebenprojekt lohnt. Die 1.0-Reihe erscheint derzeit als Release Candidates. v1.0.0-rc.2 wurde am 28. September 2026 veröffentlicht. Ob inzwischen eine stabile Version 1.0 erschienen ist, sehen Sie auf der Releases-Seite von Waku.
Das Wichtigste in Kürze
- Waku ist ein minimalistisches React-Framework auf Basis von Vite. Es bietet dateibasiertes Routing in
src/pagesund pro Layout und Page einengetConfig-Export, derrender: 'static'oderrender: 'dynamic'zurückgibt. - Layouts, Pages und Slices werden in Waku standardmäßig statisch gerendert. Eine Page wird nur dann bei jedem Request gerendert, wenn ihr
getConfigden Wertrender: 'dynamic'zurückgibt. - Eine statische Segment-Route wie
src/pages/releases/[slug].tsxmuss ausgetConfigeinstaticPaths-Array zurückgeben. - Waku wird standardmäßig auf Node.js deployt und unterstützt außerdem rein statische Ausgabe, Vercel, Netlify und Cloudflare Workers. Deno Deploy, Bun und AWS Lambda sind als experimentell gekennzeichnet.
- Waku 1.0 befand sich mit v1.0.0-rc.2 (28. September 2026) im Release-Candidate-Stadium. Es eignet sich daher gut für kleine Projekte, bei denen Sie die üblichen Änderungen in Vorabversionen in Kauf nehmen.
Wie erstellt man ein Waku-React-Projekt?
Ein neues Waku-React-Projekt legen Sie mit npm create waku@latest an. Danach arbeiten Sie mit drei CLI-Befehlen: waku dev, waku build und waku start.
npm create waku@latest
# then, inside the project:
npx waku dev # local dev server
npx waku build # production build
npx waku start # serve the production build
Laut der Getting-Started-Dokumentation von Waku werden die Node.js-Versionen ^26.0.0, ^24.0.0 und ^22.15.0 unterstützt. Intern verwendet Waku das offizielle Vite-Plugin @vitejs/plugin-rsc, um Server Components zu bundeln. Deshalb enthält die Konfigurationsdatei einen vite-Schlüssel für Plugins.
Eine asynchrone Page, die Daten lädt
Eine Page ist in Waku eine Datei in src/pages mit einer Komponente als Default-Export. Diese Komponente kann eine asynchrone Server Component sein, die Daten direkt per await lädt. Wenn Sie Data Fetching mit Server Components aus Next.js kennen, wird Ihnen der Komponentenrumpf vertraut vorkommen. Neu ist lediglich der benannte Export getConfig.
Damit das Beispiel nicht von einer externen API abhängt, liest es eine lokale JSON-Datei. Laut der Waku-Dokumentation können Server Components Dateien aus einem private-Ordner im Projektstammverzeichnis gefahrlos lesen.
// private/releases.json
[
{ "slug": "v2-0", "version": "2.0.0", "summary": "New plugin API." },
{ "slug": "v1-9", "version": "1.9.0", "summary": "Bug fixes." }
]
// src/pages/index.tsx
import { readFile } from 'node:fs/promises';
type Release = { slug: string; version: string; summary: string };
export default async function HomePage() {
const releases: Release[] = JSON.parse(
await readFile('./private/releases.json', 'utf8'),
);
return (
<>
<title>Release notes</title>
<h1>Release notes</h1>
<ul>
{releases.map((release) => (
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
</li>
))}
</ul>
</>
);
}
export const getConfig = async () => {
return { render: 'dynamic' } as const;
};
Da getConfig den Wert render: 'dynamic' zurückgibt, wird diese Page bei jedem Request auf dem Server gerendert. Ohne diese Zeile würde sie zur Build-Zeit vorgerendert. Das <title>-Tag funktioniert, weil Waku title-, meta- und link-Tags automatisch in den <head> des Dokuments verschiebt.
Eine Client Component mit ‘use client’ hinzufügen
Steht 'use client' am Anfang einer Datei, wird diese zur Grenze zwischen Server und Client, sobald eine Server Component sie importiert. Ab dieser Stelle wird jede Komponente, die die Datei einbindet, hydriert und läuft zusätzlich im Browser. Es handelt sich um dieselbe Direktive und dieselbe Regel, die Sie bereits aus dem App Router kennen.
// src/components/like-button.tsx
'use client';
import { useState } from 'react';
export const LikeButton = () => {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes((n) => n + 1)}>👍 {likes}</button>;
};
Client Components können keine Server Components importieren. Serverseitig gerenderte Inhalte lassen sich trotzdem an sie übergeben, und zwar als children oder als andere Prop:
// src/components/collapsible.tsx
'use client';
import { useState, type ReactNode } from 'react';
export const Collapsible = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{open ? 'Hide' : 'Details'}</button>
{open && children}
</div>
);
};
// src/pages/index.tsx (inside the map, with both components imported)
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
<LikeButton />
<Collapsible>
<p>{release.summary}</p>
</Collapsible>
</li>
Der Absatz mit der Zusammenfassung wird auf dem Server gerendert und als Prop an Collapsible übergeben. Die Client-Datei importiert ihn nie selbst. Ein häufiges Missverständnis bei React Server Components betrifft die Rolle von 'use server'. Laut Waku-Dokumentation kennzeichnet diese Direktive Server Actions und keine Server Components. Sie gehört also nicht an den Anfang einer Server-Component-Datei. Auch Client Components werden vor der Hydrierung auf dem Server zu HTML gerendert. Ein Session Replay einer solchen Seite zeigt, ob der Like-Button schon auf den ersten Klick reagiert oder erst nach abgeschlossener Hydrierung.
Routing: Layouts, dynamische Segmente und Render-Modi
Waku rendert Layouts, Pages und Slices statisch, sofern Sie nichts anderes festlegen. Bei API-Handlern ist es umgekehrt: Sie sind standardmäßig dynamisch. Den Render-Modus legen Sie pro Datei fest. Ein statisches Layout kann also eine dynamische Page umschließen.
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
Eine _layout.tsx-Datei gilt für die eigene Route und für alle darunter verschachtelten Routen. Ihre Komponente muss eine children-Prop entgegennehmen:
// src/pages/_layout.tsx
import type { ReactNode } from 'react';
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<>
<header><a href="/">Release notes</a></header>
<main>{children}</main>
</>
);
}
export const getConfig = async () => {
return { render: 'static' } as const;
};
Dateien mit eckigen Klammern im Namen sind Segment-Routen. Der Wert des Segments wird als Prop übergeben und lässt sich mit PageProps aus waku/router typisieren. Ist eine Segment-Route statisch, muss getConfig laut der Routing-Dokumentation von Waku ein staticPaths-Array mit den vorzurendernden Werten zurückgeben. Da getConfig asynchron sein kann, darf diese Liste aus Daten erzeugt werden:
// src/pages/releases/[slug].tsx
import { readFile } from 'node:fs/promises';
import type { PageProps } from 'waku/router';
type Release = { slug: string; version: string; summary: string };
const loadReleases = async (): Promise<Release[]> =>
JSON.parse(await readFile('./private/releases.json', 'utf8'));
export default async function ReleasePage({ slug }: PageProps<'/releases/[slug]'>) {
const release = (await loadReleases()).find((r) => r.slug === slug);
if (!release) return <p>Release not found.</p>;
return (
<>
<title>{`Release ${release.version}`}</title>
<h1>{release.version}</h1>
<p>{release.summary}</p>
</>
);
}
export const getConfig = async () => {
const releases = await loadReleases();
return { render: 'static', staticPaths: releases.map((r) => r.slug) } as const;
};
Slices, Interceptors und Hono-Middleware stehen ebenfalls zur Verfügung, wenn Sie sie brauchen. Diese kleine App kommt jedoch ohne sie aus.
Wo lässt sich Waku deployen?
Waku wird standardmäßig auf Node.js deployt. Außerdem unterstützt es rein statische Ausgabe, Vercel, Netlify und Cloudflare Workers. Die Deployment-Dokumentation von Waku kennzeichnet Deno Deploy, Bun und AWS Lambda als experimentell.
- Node.js:
waku startstartet den Produktionsserver. Für eine eigenständige Kopie liefern Sie den Ordnerdistaus und führennode dist/serve-node.jsaus. - Reines SSG: Laden Sie
dist/publicauf einen beliebigen Static Host hoch. - Vercel:
vercel - Netlify:
NETLIFY=1 npm run build, danachnetlify deploy - Cloudflare Workers:
CLOUDFLARE=1 npm run build, danachwrangler deploy - Deno Deploy, Bun, AWS Lambda (experimentell): Importieren Sie
waku/adapters/deno,waku/adapters/bunoderwaku/adapters/aws-lambdainsrc/waku.server.tsx.
Bei rein statischer Ausgabe entfällt alles, was zur Request-Zeit einen Server benötigt: dynamisches Rendering, Server Actions und API-Routen. Die oben gezeigte dynamische Startseite bräuchte daher render: 'static', bevor sie sich als SSG deployen ließe.
Wann ist Next.js weiterhin die bessere Wahl?
Next.js ist die sicherere Wahl, wenn Sie ein stabiles Release benötigen. Waku 1.0 war mit v1.0.0-rc.2 noch ein Release Candidate, und die Release Notes zu rc.2 enthalten den Eintrag „breaking: drop deprecated apis“. Rechnen Sie also mit Änderungen zwischen den Versionen. Next.js hat außerdem ein deutlich größeres Ökosystem, mehr Hosting-Integrationen und mehr Tutorials. Zudem bringt es mehr Funktionen von Haus aus mit, die Sie sich sonst selbst zusammenstellen müssten. Laut der Waku-Dokumentation hängt die Wahl von der gewünschten Architektur ab und nicht von der Projektgröße. Waku hält das Framework selbst schlank und überlässt den Rest Bibliotheken aus dem Ökosystem. Umfangreichere Frameworks übernehmen dagegen mehr dieser Aufgaben selbst. Wenn Sie möchten, dass das Framework solche Entscheidungen für Sie trifft, bleiben Sie bei Next.js.
Fazit
Waku bietet Ihnen das Server-Component-Modell, das Sie aus dem App Router kennen, mit deutlich weniger Framework drumherum. Dazu gehören asynchrone Pages, dieselbe 'use client'-Grenze, Komposition über children und ein getConfig-Export, der die Wahl zwischen statischem und dynamischem Rendering für jede Datei explizit macht. Als nächster Schritt bietet sich an, npm create waku@latest auszuführen und eine kleine Next.js-Route damit nachzubauen. Pinnen Sie dabei die exakte RC-Version in der package.json, damit ein künftiger Release Candidate das Verhalten Ihrer App nicht unbemerkt ändert.
FAQs
Wie füge ich in Waku eine API-Route hinzu?
Legen Sie eine Datei unter src/pages/_api an und exportieren Sie für jede HTTP-Methode, die die Route verarbeiten soll, eine Funktion, etwa GET, POST, PUT, PATCH oder DELETE. Die Route ergibt sich aus dem Dateinamen. Jeder Handler nimmt einen Standard-Request entgegen und gibt eine Standard-Response zurück. API-Handler werden standardmäßig dynamisch gerendert. Gibt getConfig den Wert render 'static' zurück, wird der Handler zur Build-Zeit vorgerendert. Das eignet sich etwa für Ausgaben wie einen RSS-Feed.
Wie funktionieren Umgebungsvariablen in Waku?
Serverseitiger Code liest Variablen mit der Funktion getEnv, die aus 'waku' importiert wird. In Node.js-Umgebungen funktioniert zusätzlich process.env. Client Components können nur Variablen mit dem Präfix WAKU_PUBLIC_ lesen, und zwar über import.meta.env, zum Beispiel import.meta.env.WAKU_PUBLIC_HELLO. Alles mit diesem Präfix wird im Klartext in das JavaScript-Bundle für die Produktion übernommen. Versehen Sie deshalb niemals einen API-Schlüssel oder ein anderes Secret mit dem Präfix WAKU_PUBLIC_.
Sollte ich für interne Navigation Anchor-Tags oder die Link-Komponente verwenden?
Verwenden Sie für interne Links die Link-Komponente, die aus 'waku' importiert wird. Ihre to-Prop akzeptiert entweder einen Routen-String oder ein strukturiertes Objekt mit to, params, search und hash. Die Navigation erfolgt dann clientseitig über den Router von Waku. Ein einfaches Anchor-Tag löst dagegen eine normale Browser-Navigation aus. Für programmatische Navigation oder um den aktuellen Pfad und die Query auszulesen, rufen Sie in einer Client Component den Hook useRouter aus 'waku' auf.
Sind Server Actions in Waku standardmäßig sicher?
Nein. Eine mit 'use server' gekennzeichnete Funktion wird zu einem Endpunkt, den der Client aufrufen kann. Die Waku-Dokumentation weist darauf hin, dass diese Endpunkte ungeschützt sind, solange Sie keine Authentifizierungs- und Autorisierungsprüfungen in der Funktion selbst implementieren. Prüfen Sie in jeder Server Action die Identität und die Berechtigungen des Benutzers. Fügen Sie die Direktive außerdem nur Funktionen hinzu, die Sie bewusst freigeben möchten, damit keine unbeabsichtigten Endpunkte entstehen.
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