Waku : les React Server Components sans Next.js
Découvrez Waku, un framework React léger pour utiliser les composants serveur React sans Next.js. Créez une petite application et explorez routage, rendu et déploiement.
Waku est un framework React léger, construit sur Vite, qui exécute les composants serveur et les actions serveur de React 19. L’essentiel à apprendre se résume au routage basé sur les fichiers dans src/pages et à un export getConfig, présent dans chaque page, qui choisit entre rendu statique et rendu dynamique.
Si vous n’avez utilisé les composants serveur qu’avec l’App Router de Next.js, il est difficile de distinguer ce qui relève de React de ce qui relève de Next. Waku conserve le modèle de React et retire l’essentiel du framework qui l’entoure. Cet article construit une petite application, de l’initialisation au déploiement, pour vous aider à décider si Waku mérite d’être testé sur un projet personnel. La branche 1.0 est publiée sous forme de release candidates. La version v1.0.0-rc.2 a été publiée le 28 septembre 2026. Consultez donc la page des versions de Waku pour vérifier si une version 1.0 stable a suivi.
Points clés à retenir
- Waku est un framework React minimaliste basé sur Vite. Il propose un routage basé sur les fichiers dans
src/pageset un exportgetConfigpar layout et par page, qui renvoierender: 'static'ourender: 'dynamic'. - Dans Waku, les layouts, les pages et les slices sont rendus de manière statique par défaut. Une page n’est rendue à chaque requête que si son
getConfigrenvoierender: 'dynamic'. - Une route à segment statique comme
src/pages/releases/[slug].tsxdoit renvoyer un tableaustaticPathsdepuisgetConfig. - Waku se déploie par défaut sur Node.js et prend aussi en charge la génération purement statique, Vercel, Netlify et Cloudflare Workers. Deno Deploy, Bun et AWS Lambda sont signalés comme expérimentaux.
- Waku 1.0 en était au stade de release candidate avec la v1.0.0-rc.2 (28 septembre 2026). Il convient donc aux petits projets pour lesquels vous acceptez l’instabilité propre aux préversions.
Comment créer un projet React avec Waku ?
Pour créer un projet React avec Waku, exécutez npm create waku@latest. Vous utiliserez ensuite trois commandes CLI : waku dev, waku build et 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
Selon la documentation de démarrage de Waku, les versions de Node.js prises en charge sont ^26.0.0, ^24.0.0 ou ^22.15.0. En interne, Waku s’appuie sur le plugin officiel de Vite @vitejs/plugin-rsc pour bundler les composants serveur. C’est pourquoi son fichier de configuration comporte une clé vite destinée aux plugins.
Une page asynchrone qui récupère des données
Dans Waku, une page est un fichier situé dans src/pages qui exporte un composant par défaut. Ce composant peut être un composant serveur asynchrone qui attend directement les données avec await. Si vous connaissez déjà la récupération de données avec les composants serveur dans Next.js, le corps du composant vous semblera identique. La différence tient à l’export nommé getConfig.
Pour éviter toute dépendance à une API externe, l’exemple lit un fichier JSON local. D’après la documentation de Waku, les fichiers placés dans un dossier private à la racine du projet peuvent être lus en toute sécurité depuis les composants serveur.
// 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;
};
Comme getConfig renvoie render: 'dynamic', cette page est rendue côté serveur à chaque requête. Sans cette ligne, elle serait prérendue au moment du build. La balise <title> fonctionne parce que Waku remonte (hoisting) les balises title, meta et link dans le <head> du document.
Ajouter un composant client avec ‘use client’
Placer 'use client' en tête d’un fichier en fait une frontière serveur-client dès qu’un composant serveur l’importe. À partir de là, tous les composants importés par ce fichier sont hydratés et s’exécutent aussi dans le navigateur. Il s’agit de la même directive et de la même règle que celles que vous utilisez déjà dans l’App Router.
// 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>;
};
Les composants client ne peuvent pas importer de composants serveur. Le rendu serveur peut toutefois leur parvenir si vous le transmettez via children ou une autre 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>
Le paragraphe de résumé est rendu sur le serveur, puis transmis à Collapsible sous forme de prop. Le fichier client ne l’importe jamais. Le rôle de 'use server' est une source de confusion fréquente avec les React Server Components. Dans la documentation de Waku, cette directive désigne les actions serveur, et non les composants serveur. Elle n’a donc pas sa place en tête d’un fichier de composant serveur. Les composants client sont malgré tout rendus en HTML côté serveur avant d’être hydratés. Le session replay d’une page comme celle-ci permet de voir si le bouton « J’aime » réagit dès le premier clic ou reste inerte jusqu’à la fin de l’hydratation.
Routage : layouts, segments dynamiques et modes de rendu
Waku effectue un rendu statique des layouts, des pages et des slices, sauf indication contraire. Les gestionnaires d’API fonctionnent à l’inverse : ils sont dynamiques par défaut. Le mode de rendu se définit fichier par fichier, si bien qu’un layout statique peut envelopper une page dynamique.
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
Un fichier _layout.tsx s’applique à sa propre route et à toutes les routes imbriquées. Son composant doit accepter une prop children :
// 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;
};
Les fichiers entre crochets sont des routes à segment. La valeur du segment est transmise sous forme de prop, que vous pouvez typer avec PageProps depuis waku/router. Lorsqu’une route à segment est statique, la documentation de routage de Waku impose que getConfig renvoie un tableau staticPaths listant les valeurs à prérendre. Comme getConfig peut être asynchrone, cette liste peut provenir des données :
// 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;
};
Les slices, les intercepteurs et les middlewares Hono sont également disponibles si vous en avez besoin, mais cette petite application n’en utilise aucun.
Où déployer Waku ?
Waku se déploie par défaut sur Node.js. Il prend aussi en charge la génération purement statique, Vercel, Netlify et Cloudflare Workers. La documentation de déploiement de Waku signale Deno Deploy, Bun et AWS Lambda comme expérimentaux.
- Node.js :
waku startlance le serveur de production. Pour une copie autonome, déployez le dossierdistet exécuteznode dist/serve-node.js. - SSG pur : téléversez
dist/publicsur n’importe quel hébergeur statique. - Vercel :
vercel - Netlify :
NETLIFY=1 npm run build, puisnetlify deploy - Cloudflare Workers :
CLOUDFLARE=1 npm run build, puiswrangler deploy - Deno Deploy, Bun, AWS Lambda (expérimental) : importez
waku/adapters/deno,waku/adapters/bunouwaku/adapters/aws-lambdadanssrc/waku.server.tsx.
La génération purement statique supprime tout ce qui nécessite un serveur au moment de la requête : le rendu dynamique, les actions serveur et les routes d’API. La page d’accueil dynamique présentée plus haut devrait donc passer à render: 'static' avant de pouvoir être déployée en SSG.
Dans quels cas Next.js reste-t-il le meilleur choix ?
Next.js est le choix le plus sûr si vous avez besoin d’une version stable. Waku 1.0 n’était encore qu’une release candidate avec la v1.0.0-rc.2, et les notes de version de la rc.2 mentionnent « breaking: drop deprecated apis ». Attendez-vous donc à des changements d’une version à l’autre. Next.js dispose aussi d’un écosystème bien plus vaste, de davantage d’intégrations avec les hébergeurs et de plus de tutoriels. Il intègre également plus de fonctionnalités natives, que vous devriez sinon assembler vous-même. Selon la documentation de Waku, le choix dépend de l’architecture souhaitée, et non de la taille du projet. Waku garde un noyau léger et s’appuie sur les bibliothèques de l’écosystème pour le reste, alors que des frameworks plus complets prennent eux-mêmes en charge une plus grande partie de ce travail. Si vous préférez que le framework tranche ces questions à votre place, restez sur Next.js.
Conclusion
Waku vous offre le modèle de composants serveur que vous connaissez avec l’App Router, avec beaucoup moins de framework autour : des pages asynchrones, la même frontière 'use client', la composition via children et un export getConfig qui rend explicite, fichier par fichier, le choix entre rendu statique et rendu dynamique. Pour aller plus loin, exécutez npm create waku@latest et reconstruisez une petite route Next.js avec Waku. Épinglez aussi la version RC exacte dans package.json, afin qu’une future release candidate ne modifie pas le comportement de votre application à votre insu.
FAQ
Comment ajouter une route d'API dans Waku ?
Ajoutez un fichier sous src/pages/_api et exportez une fonction pour chaque méthode HTTP que la route doit gérer, comme GET, POST, PUT, PATCH ou DELETE. La route est déduite du nom du fichier. Chaque gestionnaire reçoit un objet Request standard et renvoie un objet Response standard. Les gestionnaires d'API sont rendus dynamiquement par défaut. Un getConfig qui renvoie render 'static' prérend le gestionnaire au moment du build, ce qui convient à des sorties comme un flux RSS.
Comment fonctionnent les variables d'environnement dans Waku ?
Le code serveur lit les variables avec la fonction getEnv importée depuis 'waku', et process.env fonctionne également dans les environnements Node.js. Les composants client ne peuvent lire que les variables portant le préfixe WAKU_PUBLIC_, via import.meta.env, par exemple import.meta.env.WAKU_PUBLIC_HELLO. Toute variable portant ce préfixe est incluse en clair dans le bundle JavaScript de production. N'appliquez donc jamais le préfixe WAKU_PUBLIC_ à une clé d'API ou à tout autre secret.
Faut-il utiliser des balises d'ancrage ou le composant Link pour la navigation interne dans Waku ?
Utilisez le composant Link importé depuis 'waku' pour les liens internes. Sa prop to accepte soit une chaîne de route, soit un objet structuré avec to, params, search et hash. La navigation s'effectue alors côté client via le routeur de Waku. Une simple balise d'ancrage déclenche au contraire une navigation classique du navigateur. Pour naviguer par programmation ou lire le chemin et les paramètres de requête actuels, appelez le hook useRouter de 'waku' dans un composant client.
Les actions serveur de Waku sont-elles sécurisées par défaut ?
Non. Une fonction marquée avec 'use server' devient un endpoint que le client peut appeler. La documentation de Waku prévient que rien ne protège ces endpoints si vous n'écrivez pas vous-même les contrôles d'authentification et d'autorisation dans la fonction. Vérifiez l'identité et les permissions de l'utilisateur dans chaque action serveur, et n'ajoutez la directive qu'aux fonctions que vous souhaitez réellement exposer, pour éviter de créer des endpoints involontairement.
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