fr

Routage React + Vite : toutes les options en 2026

Guide complet et sourcé des solutions de routage pour React et Preact avec Vite : React Router v7, TanStack Router, Wouter, Generouted, vite-plugin-pages et preact-iso.

Tu développes une SPA avec Vite et React (ou Preact). Tu as besoin de routes. Tu as entendu parler de React Router, mais aussi de TanStack Router, de file-based routing “à la Next.js”, et d’alternatives minimalistes. Cet article passe en revue six solutions, leurs fonctionnements internes, et dans quel cas choisir laquelle.


1. Comment fonctionne le routage côté client

Avant de comparer les outils, un rappel sur le mécanisme sous-jacent. Tous les routeurs SPA fonctionnent sur le même principe :

  1. L’utilisateur clique sur un lien ou modifie l’URL
  2. Le routeur intercepte la navigation (via popstate ou click handler sur les <a>)
  3. Il compare l’URL à un arbre de routes déclarées
  4. Il rend le composant correspondant — sans rechargement de page
Clic sur <Link to="/posts/42">


┌──────────────────────┐
│  Interception (push   │
│  History API)         │
└──────────┬───────────┘


┌──────────────────────┐
│  Matching : /posts/42 │
│  → Route /posts/:id   │
└──────────┬───────────┘


┌──────────────────────┐
│  Rendu du composant   │
│  <PostDetail id={42}> │
└──────────────────────┘

La différence entre les routeurs se joue sur trois axes :

  • Déclaration des routes : impérative (dans le code) vs file-based (convention de fichiers)
  • Chargement de données : intégré (loaders/actions) vs externe (à gérer soi-même)
  • Type-safety : les chemins sont-ils vérifiés à la compilation ?

2. React Router v7 — le standard de facto

Version actuelle : 7.14.0 Source : React Router Documentation

React Router v7 est le résultat de la fusion avec Remix. Il fonctionne désormais selon trois modes, ce qui est à la fois sa force et sa complexité.

Les trois modes

Library Mode — le mode classique. On déclare les routes dans le code, pas de plugin Vite spécifique nécessaire :

import { BrowserRouter, Routes, Route } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/posts/:id" element={<Post />} />
      </Routes>
    </BrowserRouter>
  );
}

Data Mode — ajoute les loader et action hérités de Remix. Les données sont liées aux routes :

import { createBrowserRouter, RouterProvider } from "react-router";

const router = createBrowserRouter([
  {
    path: "/posts/:id",
    loader: async ({ params }) => {
      return fetch(`/api/posts/${params.id}`);
    },
    Component: Post,
  },
]);

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

Dans le composant, on accède aux données via useLoaderData(). L’avantage : le chargement commence avant le rendu du composant, ce qui élimine les cascades de waterfalls.

Framework Mode — le mode full-stack avec file-based routing, SSR et pre-rendering. Nécessite le plugin Vite dédié :

// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  plugins: [reactRouter()],
});

Les routes sont alors définies par la structure de fichiers dans app/routes/ :

app/routes/
├── _index.tsx        → /
├── posts.tsx         → /posts (layout)
├── posts._index.tsx  → /posts/
└── posts.$id.tsx     → /posts/:id

Chaque fichier route est un module avec des exports nommés. Le framework génère automatiquement les types via ./+types/<routeName> :

// app/routes/posts.$id.tsx
import type { Route } from "./+types/posts.$id";
import { Form, redirect } from "react-router";

// Chargement de données côté serveur — exécuté avant le rendu
export async function loader({ params }: Route.LoaderArgs) {
  const post = await db.post.findUnique({ where: { id: params.id } });
  if (!post) throw new Response("Not found", { status: 404 });
  return { post };
}

// Gestion des soumissions de formulaire
export async function action({ params, request }: Route.ActionArgs) {
  const formData = await request.formData();
  await db.post.update({
    where: { id: params.id },
    data: { title: formData.get("title") },
  });
  return redirect(`/posts/${params.id}`);
}

// Le composant — export default, reçoit loaderData typé automatiquement
export default function Post({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>{loaderData.post.title}</h1>
      <Form method="post">
        <input name="title" defaultValue={loaderData.post.title} />
        <button type="submit">Modifier</button>
      </Form>
    </div>
  );
}

// Gestion d'erreurs pour cette route
export function ErrorBoundary() {
  return <div>Erreur lors du chargement du post.</div>;
}

Les exports disponibles sont : loader, action, clientLoader, clientAction, default (composant), et ErrorBoundary. Tout est optionnel — un fichier peut n’exporter qu’un composant par défaut.

Source : React Router — Picking a Mode

Fonctionnement interne

Le principle : React Router maintient un arbre de routes en mémoire. À chaque changement d’URL, il parcourt cet arbre en profondeur pour trouver la branche correspondante. En mode Data/Framework, il lance les loader de toutes les routes matchées en parallèle avant de rendre quoi que ce soit.

En mode Framework, le type-safety est automatique : le plugin génère des types TypeScript pour les params, les loaders et les actions de chaque route.

Pour et contre

PourContre
Écosystème le plus large, documentation abondanteTrois modes = confusion pour les débutants
Loaders/actions éprouvés (héritage Remix)Migration v6 → v7 non triviale
SSR complet en mode FrameworkMode Framework couplé au plugin Vite spécifique
~14-18 kB gzipped en mode LibraryLe mode Library seul n’apporte rien de nouveau

Compatibilité Preact

Non. React Router v7 dépend d’APIs internes de React 18/19 (transitions, Suspense). Aucun support officiel, aucun alias preact/compat fiable.

Source : Discussion Remix + Preact


3. TanStack Router — TypeScript d’abord

Version actuelle : 1.168.x Source : TanStack Router Documentation

TanStack Router est le routeur de Tanner Linsley (l’auteur de React Query). Son parti pris : tout est typé, du chemin de route aux search params.

Déclaration des routes

En code :

import {
  createRouter,
  createRoute,
  createRootRoute,
} from "@tanstack/react-router";

const rootRoute = createRootRoute({ component: Root });

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/posts",
  component: Posts,
});

const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: "$id",
  component: Post,
  // Search params validés par schéma
  validateSearch: (search) => ({
    tab: (search.tab as "comments" | "related") || "comments",
  }),
});

const router = createRouter({
  routeTree: rootRoute.addChildren([postsRoute.addChildren([postRoute])]),
});

En file-based (via le plugin Vite) :

// vite.config.ts
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react()],
});

Le plugin scanne src/routes/ et génère routeTree.gen.ts. La structure est similaire à Next.js :

src/routes/
├── __root.tsx        → layout racine
├── index.tsx         → /
├── posts/
│   ├── index.tsx     → /posts
│   └── $id.tsx       → /posts/:id

Chaque fichier exporte une constante Route créée avec createFileRoute. Le path passé en argument doit correspondre à la position du fichier dans l’arbre :

// src/routes/posts/$id.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/$id")({
  // Search params validés — typés automatiquement dans le composant
  validateSearch: (search) => ({
    tab: (search.tab as "comments" | "related") || "comments",
    page: Number(search.page ?? 1),
  }),

  // loaderDeps : déclare quels search params déclenchent un re-fetch
  loaderDeps: ({ search }) => ({ page: search.page }),

  loader: async ({ params, deps }) => {
    const res = await fetch(`/api/posts/${params.id}?page=${deps.page}`);
    return res.json();
  },

  pendingComponent: () => <div>Chargement...</div>,

  component: function Post() {
    const post = Route.useLoaderData(); // typé depuis le retour du loader
    const { tab, page } = Route.useSearch(); // typé depuis validateSearch
    return (
      <div>
        <h1>{post.title}</h1>
        <p>
          Onglet : {tab}, Page : {page}
        </p>
      </div>
    );
  },
});

Le fichier __root.tsx utilise createRootRoute (pas createFileRoute) et contient le layout global avec <Outlet /> :

// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from "@tanstack/react-router";

export const Route = createRootRoute({
  component: function Root() {
    return (
      <>
        <nav>
          <Link to="/">Accueil</Link>
          <Link to="/posts">Posts</Link>
        </nav>
        <Outlet />
      </>
    );
  },
});

Source : TanStack Router — File-Based Routing

Le différenciateur : les search params

Là où les autres routeurs traitent les query strings comme un afterthought (chaîne brute via useSearchParams()), TanStack Router en fait des citoyens de première classe :

const postRoute = createRoute({
  path: "$id",
  validateSearch: z.object({
    tab: z.enum(["comments", "related"]).default("comments"),
    page: z.number().default(1),
  }),
});

// Dans le composant : tout est typé
const { tab, page } = postRoute.useSearch();

// Navigation : autocomplétion et validation
navigate({ search: { tab: "related", page: 2 } });

C’est un vrai avantage pour les applications avec des filtres, de la pagination, ou des états partagés via l’URL.

Fonctionnement interne

TanStack Router utilise un système de route matching structural plutôt que du pattern matching sur des strings. L’arbre de routes est construit à la compilation (en mode file-based) ou à l’initialisation (en mode code). Chaque route est un objet typé, et la navigation est vérifiée par le système de types TypeScript — un lien vers une route inexistante ou avec des params manquants est une erreur de compilation.

Les loaders intègrent un cache SWR (Stale-While-Revalidate) : les données sont servies immédiatement depuis le cache, puis rafraîchies en arrière-plan.

Pour et contre

PourContre
Type-safety de bout en bout (le meilleur du marché)Courbe d’apprentissage plus raide
Search params comme citoyens de première classeÉcosystème plus jeune que React Router
Cache SWR intégré aux loadersVersions fréquentes (1.168.x — API stable mais rythme rapide)
Devtools dédiéesTanStack Start (SSR) encore en beta
~12 kB gzipped

Compatibilité Preact

Non. Les mainteneurs ont explicitement indiqué ne pas prévoir de support Preact.

Source : TanStack Router — Discussion Preact


4. Wouter — le minimaliste

Version actuelle : 3.9.0 Source : Wouter — GitHub

Wouter est un routeur qui tient en 2.1 kB gzipped. Pas de provider obligatoire, pas de configuration — juste des composants et des hooks.

Utilisation

import { Route, Switch, Link } from "wouter";

function App() {
  return (
    <>
      <nav>
        <Link href="/posts">Posts</Link>
      </nav>
      <Switch>
        <Route path="/" component={Home} />
        <Route path="/posts" component={Posts} />
        <Route path="/posts/:id" component={Post} />
        <Route>404 — Page introuvable</Route>
      </Switch>
    </>
  );
}

// Accès aux params via les props
function Post({ params }) {
  return <h1>Post {params.id}</h1>;
}

Pas besoin de <BrowserRouter> ou de <RouterProvider>. Wouter fonctionne directement — il utilise l’API window.location et popstate sans contexte global.

Hooks disponibles

import { useRoute, useLocation, useSearch } from "wouter";

// Pattern matching sur la route courante
const [match, params] = useRoute("/posts/:id");

// Navigation programmatique
const [location, setLocation] = useLocation();
setLocation("/posts/42");

// Query string (depuis v3)
const searchString = useSearch(); // "?tab=comments" → "tab=comments"

Fonctionnement interne

Wouter utilise un path matching basé sur URLPattern (avec fallback regex pour les navigateurs plus anciens). Il n’y a pas d’arbre de routes centralisé — chaque composant <Route> écoute indépendamment les changements d’URL via un hook useLocation. C’est ce qui permet de ne pas avoir de provider : les routes sont réactives par observation directe de window.location.

Le <Switch> est optionnel mais recommandé : il stoppe l’évaluation à la première route qui matche (comportement exclusif), ce qui évite de rendre plusieurs routes simultanément.

SSR

Supporté via des props dédiées :

<Router ssrPath="/posts/42" ssrSearch="tab=comments">
  <App />
</Router>

C’est basique mais fonctionnel — pas de streaming, pas de loaders côté serveur.

Pour et contre

PourContre
2.1 kB gzipped — le plus légerPas de loaders/actions
Zero configuration, pas de providerPas de file-based routing
API simple et familièreType-safety limitée (pas de vérification des chemins)
SSR basique intégréCode-splitting à gérer manuellement
Nested routing (depuis v3)Pas de devtools

Compatibilité Preact

Oui, officielle. Package dédié :

npm install wouter-preact
import { Route, Link } from "wouter-preact";

API identique, même taille. C’est l’une des deux options solides pour Preact (avec preact-iso).


5. File-based routing pour Vite — Generouted et vite-plugin-pages

Ces deux outils ne sont pas des routeurs : ce sont des générateurs de routes basés sur le système de fichiers, qui produisent une configuration pour un routeur existant.

5.1 Generouted

Version actuelle : 1.20.0 Source : Generouted — GitHub

Generouted fonctionne au-dessus de React Router ou TanStack Router. Le plugin Vite scanne src/pages/ et génère automatiquement l’arbre de routes.

// vite.config.ts
import generouted from "@generouted/react-router/plugin";

export default defineConfig({
  plugins: [react(), generouted()],
});
src/pages/
├── index.tsx            → /
├── posts/
│   ├── index.tsx        → /posts
│   └── [id].tsx         → /posts/:id
├── _layout.tsx          → layout partagé
└── +modal.tsx           → modale globale

Le contenu d’un fichier page est un simple export default — Generouted se charge du reste :

// src/pages/posts/[id].tsx
export default function Post() {
  // On utilise les hooks du routeur sous-jacent (React Router ou TanStack)
  const { id } = useParams();
  return <h1>Post {id}</h1>;
}

Pour les loaders (si le routeur sous-jacent est React Router) :

// src/pages/posts/[id].tsx
export const Loader = async ({ params }) => {
  return fetch(`/api/posts/${params.id}`);
};

export default function Post() {
  const data = useLoaderData();
  return <h1>{data.title}</h1>;
}

Le point d’entrée de l’app devient minimal :

// src/main.tsx
import { Routes } from "@generouted/react-router";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<Routes />);

Le gros plus : Generouted génère un fichier src/router.ts avec des utilitaires type-safe. Les chemins de routes sont vérifiés à la compilation :

import { Link, useNavigate } from "src/router";

<Link to="/posts/:id" params={{ id: "42" }} />  // ✅ OK
<Link to="/inexistant" />                         // ❌ Erreur TypeScript

Limites :

  • Pas de SSR (client-side uniquement)
  • Dernière release il y a ~8 mois — la pérennité est à surveiller
  • Pas de support Preact

5.2 vite-plugin-pages

Version actuelle : 0.33.3 Source : vite-plugin-pages — GitHub

Plus ancien et plus établi que Generouted, ce plugin utilise import.meta.glob de Vite pour scanner les fichiers et générer des routes pour React Router.

// vite.config.ts
import Pages from "vite-plugin-pages";

export default defineConfig({
  plugins: [react(), Pages({ resolver: "react" })],
});
// src/main.tsx
import { useRoutes } from "react-router";
import routes from "~react-pages";

function App() {
  return useRoutes(routes);
}

Trois styles de nommage configurables :

StyleParamètre dynamiqueCatch-all
Next.js[id].tsx[...all].tsx
Nuxt_id.tsxn/a
Remix$id.tsx$.tsx

Limites :

  • Pas de type-safety sur les chemins
  • Pas de support natif des modales ou des layouts avancés
  • Pas de support Preact

Generouted vs vite-plugin-pages

AspectGeneroutedvite-plugin-pages
Routeur sous-jacentReact Router ou TanStack RouterReact Router uniquement
Type-safetyOui (chemins vérifiés)Non
LayoutsOui (_layout.tsx)Basique
Modales globalesOui (+modal.tsx)Non
MaintenanceDernière release ~8 moisActif (février 2026)

En pratique, Generouted est recommandé si tu veux du file-based routing dans un projet Vite sans passer en mode Framework de React Router. La type-safety générée est un vrai gain de productivité.


6. Routage Preact — preact-iso et alternatives

Si tu utilises Preact (et non React), l’écosystème est plus restreint mais deux options solides existent.

6.1 preact-iso — le choix par défaut

Version actuelle : 2.11.1 Source : preact-iso — Documentation

preact-iso est la solution officielle de l’équipe Preact. Le nom vient de “isomorphic” : il gère le routage, le lazy-loading, le pre-rendering et l’hydration dans un seul package.

import { LocationProvider, Router, Route } from "preact-iso";
import { lazy } from "preact-iso";

const Home = lazy(() => import("./pages/Home"));
const Post = lazy(() => import("./pages/Post"));

export function App() {
  return (
    <LocationProvider>
      <Router>
        <Route path="/" component={Home} />
        <Route path="/posts/:id" component={Post} />
        <Route default component={NotFound} />
      </Router>
    </LocationProvider>
  );
}

Fonctionnement interne : Le <Router> de preact-iso est async-aware. Quand une navigation se produit, il conserve l’ancienne route visible jusqu’à ce que le nouveau composant (potentiellement lazy-loadé) soit prêt. Pas de flash blanc entre les transitions, contrairement à un routeur naïf avec Suspense.

La fonction prerender() permet de générer du HTML statique au build-time :

import { prerender } from "preact-iso";
import { App } from "./app";

export default async function () {
  return await prerender(<App />);
}

C’est le routeur utilisé par create-preact (le scaffolder officiel).

6.2 wouter-preact

Voir la section 4. Même API que Wouter, package dédié wouter-preact. Un bon choix si tu veux un routeur ultra-léger sans les fonctionnalités isomorphes de preact-iso.

6.3 preact-router (legacy)

Version actuelle : 4.1.2 (dernière publication il y a ~3 ans)

Historiquement le routeur standard de Preact, maintenant en mode maintenance. L’équipe Preact recommande preact-iso pour les nouveaux projets.

Que choisir pour Preact ?

BesoinSolution
App Preact standard, SSR/pre-renderingpreact-iso
Routeur ultra-léger, pas besoin de SSRwouter-preact
Migration depuis un vieux projetpreact-router (mais migrer vers preact-iso si possible)

7. Tableau comparatif général

CritèreReact Router v7TanStack RouterWouterGeneroutedvite-plugin-pagespreact-iso
Version7.14.01.168.x3.9.01.20.00.33.32.11.1
ApprocheImpératif ou FSImpératif ou FSImpératif (hooks)FS (surcouche)FS (surcouche)Impératif
Type-safeOui (mode Framework)Oui (natif)NonOui (généré)NonNon
Plugin ViteOuiOuiNon nécessaireOuiOuiNon nécessaire
SSRCompletOui (+ Start en beta)BasiqueNonNonOui (isomorphe)
LoadersOuiOui (+ cache SWR)NonVia routeurNonNon
Code-splittingAuto (Framework)AutoManuelVia routeurOuiOui (lazy)
Taille gzip~14-18 kB~12 kB2.1 kB~0 (build)~0 (build)~1-2 kB
PreactNonNonOuiNonNonOui (natif)
MaturitéTrès hauteHauteMoyenneMoyenneHauteMoyenne-haute

8. Recommandations par cas d’usage

SPA React + Vite classique

React Router v7 en mode Data. C’est le choix le plus pragmatique : large écosystème, loaders/actions, documentation abondante. Le mode Data offre le meilleur rapport fonctionnalités/complexité sans imposer la couche Framework.

Projet TypeScript exigeant avec search params complexes

TanStack Router. La type-safety de bout en bout et le traitement des search params comme citoyens de première classe en font le meilleur choix pour les apps avec des filtres, de la pagination, et des états dans l’URL. La courbe d’apprentissage est justifiée par la productivité à moyen terme.

File-based routing sans Next.js

Deux options :

  • React Router v7 en mode Framework si tu veux le SSR et un écosystème officiel
  • Generouted si tu veux juste le file-based routing côté client, au-dessus du routeur de ton choix

App légère ou widget embarqué

Wouter. 2.1 kB, zero config, API simple. Pas besoin d’un routeur de 15 kB pour une app avec 5 routes et pas de data loading complexe.

Preact

preact-iso si tu as besoin de SSR ou pre-rendering. wouter-preact sinon.

Migration progressive depuis React Router v6

Rester sur React Router v7 en mode Library, puis migrer progressivement vers le mode Data pour les routes qui bénéficient de loaders. L’API est rétrocompatible.


9. Pièges et limitations connues

React Router : la confusion des modes

Le piège le plus fréquent est de mélanger les imports entre les modes. En mode Library, on utilise <BrowserRouter> + <Routes>. En mode Data, on utilise createBrowserRouter + <RouterProvider>. Les deux sont incompatibles dans la même app.

Source : React Router — Picking a Mode

TanStack Router : le fichier généré

En mode file-based, routeTree.gen.ts est régénéré automatiquement par le plugin Vite. Il ne faut jamais le modifier manuellement — les changements seront écrasés. Ce fichier doit être commité (il fait partie du build), ce qui peut surprendre.

Generouted : pérennité

Le projet est maintenu par un seul développeur. La dernière release date de ~8 mois. Pour un projet long terme, vérifier l’activité du dépôt avant de s’engager. En cas d’abandon, la migration vers le file-based routing natif de React Router v7 ou TanStack Router est le plan de repli.

Wouter : pas de loaders

Wouter ne fournit aucun mécanisme de chargement de données lié aux routes. Il faut gérer les useEffect + fetch manuellement, ou coupler avec React Query / SWR. C’est un choix assumé (minimalisme), mais ça implique de résoudre les cascades de requêtes soi-même.

Preact + alias compat : les routeurs React ne marchent pas

Même avec preact/compat, les routeurs React modernes (React Router v7, TanStack Router) utilisent des APIs internes de React (transitions, useSyncExternalStore avec des subtilités de scheduling) qui ne sont pas émulées. N’essaie pas de forcer l’alias — utilise wouter-preact ou preact-iso.