ITSkillsCenter
Blog

File-based routing avec TanStack Router : conventions, layouts et search params typés

13 دقائق للقراءة

TanStack Router lit le contenu de src/routes pour générer un arbre de routes typé sans qu’on écrive de configuration manuelle. Cette approche déclarative déplace tout le travail de routage vers l’arborescence des fichiers : un fichier devient une URL, un dossier devient un préfixe, un préfixe spécial change la sémantique. Le plugin Vite régénère routeTree.gen.ts à chaque sauvegarde, ce qui donne une autocomplétion stricte sur les paramètres et les search params. Ce tutoriel détaille les conventions à mémoriser, l’ordre du plugin dans vite.config.ts, l’écriture du root, l’usage des layouts pathless, des groupes (auth), des paramètres dynamiques, du splat catch-all et de la validation Zod sur les search params. Chaque étape contient le code minimal qui marche, plus la prose qui explique pourquoi le router se comporte ainsi et comment lire la sortie.

Cet article fait partie de la série autour de TanStack Start en production 2026, hub d’orientation de l’écosystème.

Prérequis

Avant de coder, il faut une base saine. Le file-based routing dépend du plugin Vite TanStack Router et d’une version récente de React 19. Sans le plugin chargé en premier, le fichier routeTree.gen.ts ne se met pas à jour et l’autocomplétion casse. Vérifiez les éléments suivants sur la machine de travail.

  • Node.js 22 LTS minimum, vérifié avec node -v
  • Un projet Vite 6 ou 7 déjà initialisé avec React 19
  • Les paquets @tanstack/react-router, @tanstack/react-router-devtools, @tanstack/router-plugin
  • zod installé pour la validation des search params
  • TypeScript 5.4 ou plus récent, strict: true dans tsconfig.json

Si l’un de ces éléments manque, l’installation se fait en une commande. Lancez la dans le dossier racine du projet.

npm install @tanstack/react-router @tanstack/react-router-devtools zod
npm install -D @tanstack/router-plugin

La sortie doit lister quatre paquets ajoutés sans avertissement de peerDependency. Si npm signale un conflit avec React 18, montez React et React-DOM à la version 19 avant de poursuivre. Sans cela, le router compile mais les hooks useNavigate émettront des avertissements en mode dev.

Étape 1 — Brancher le plugin dans vite.config.ts

Le plugin TanStack Router doit s’exécuter avant le plugin React, car il génère le fichier d’arbre que le compilateur lit ensuite. Inverser l’ordre produit l’erreur classique routeTree.gen.ts not found au démarrage. On déclare donc tanstackRouter en premier, on lui passe la cible react et on active le code splitting automatique pour réduire le bundle initial.

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      autoCodeSplitting: true,
    }),
    react(),
  ],
})

Au prochain npm run dev, le plugin scanne src/routes et écrit src/routeTree.gen.ts. Ce fichier est un artefact, il ne doit pas être édité à la main et il s’ajoute généralement au .gitignore si l’équipe préfère le régénérer en CI. Sa présence est le premier indicateur que la chaîne marche.

Étape 2 — Écrire la route racine __root.tsx

La racine de l’arbre s’appelle obligatoirement __root.tsx (deux underscores). Tout le reste hérite d’elle. Elle expose un Outlet où les routes enfants se rendent, et c’est l’endroit logique pour brancher les Devtools, un fournisseur global ou un layout commun. Contrairement aux autres routes, on utilise createRootRoute et non createFileRoute.

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'

export const Route = createRootRoute({
  component: () => (
    <>
      <header>
        <nav>Navigation globale</nav>
      </header>
      <main>
        <Outlet />
      </main>
      <TanStackRouterDevtools />
    </>
  ),
})

Au démarrage, l’écran affiche le header, le contenu de la première route enfant rendue dans Outlet, et le panneau Devtools en bas à droite. Si le panneau ne s’affiche pas, c’est que NODE_ENV est en production : les Devtools se masquent automatiquement hors développement.

Étape 3 — Créer la route index et une sous-route classique

Le fichier index.tsx dans un dossier représente la route exacte de ce dossier. À la racine, src/routes/index.tsx correspond à /. Pour une URL /about, on crée src/routes/about.tsx. La fonction createFileRoute reçoit le chemin entre guillemets ; le plugin remplace ce chemin à la génération si vous laissez la chaîne vide, mais l’écrire explicitement aide la lecture en revue.

// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: HomePage,
})

function HomePage() {
  return <h1>Accueil</h1>
}

Naviguez sur http://localhost:5173/ : le titre Accueil s’affiche dans la zone Outlet de la racine. Le panneau Devtools liste désormais deux routes, __root et /. C’est le retour visuel qui confirme que le plugin a vu le fichier et l’a câblé.

Étape 4 — Ajouter un paramètre dynamique avec $

Un segment d’URL variable est marqué par le préfixe $ dans le nom de fichier. Pour matcher /posts/123, on crée src/routes/posts/$postId.tsx. Le hook useParams retourne ensuite un objet typé où la clé porte exactement le nom du fichier sans le dollar. Cette discipline évite les erreurs de frappe.

// src/routes/posts/$postId.tsx
import { createFileRoute, useParams } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  component: PostPage,
})

function PostPage() {
  const { postId } = useParams({ from: '/posts/$postId' })
  return <article>Article {postId}</article>
}

Visitez /posts/42 : la page rend Article 42. Tapez /posts/abc et la valeur reçue est la chaîne abc ; les paramètres ne sont pas convertis en nombre. Si vous attendez un identifiant numérique, validez-le dans parseParams ou cast en amont du composant.

Étape 5 — Mutualiser un layout pathless avec _layout

Un fichier ou un dossier préfixé par un underscore est un layout pathless : il enveloppe ses enfants sans ajouter de segment à l’URL. C’est l’outil pour partager une sidebar, un cadre d’authentification ou une navigation secondaire entre plusieurs pages, sans inflation d’URL.

// src/routes/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_layout')({
  component: AppLayout,
})

function AppLayout() {
  return (
    <div className="shell">
      <aside>Sidebar partagée</aside>
      <section>
        <Outlet />
      </section>
    </div>
  )
}

Toute route placée sous src/routes/_layout/ hérite de cette enveloppe. L’URL reste propre, par exemple /dashboard et non /_layout/dashboard. À l’inspection, le DOM montre la aside, puis le contenu de la sous-route dans la section. C’est le pattern à privilégier pour les zones authentifiées.

Étape 6 — Protéger une zone avec admin/_authenticated

On combine maintenant un dossier classique et un layout pathless pour créer une zone admin protégée. La structure src/routes/admin/_authenticated/route.tsx produit un layout intermédiaire qui s’applique à tout ce qui se trouve sous admin/ sans changer l’URL. Le fichier s’appelle route.tsx et non _authenticated.tsx parce qu’il représente la route du dossier lui-même.

// src/routes/admin/_authenticated/route.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/admin/_authenticated')({
  beforeLoad: ({ location }) => {
    const token = localStorage.getItem('token')
    if (!token) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
  component: () => <Outlet />,
})

Le hook beforeLoad s’exécute avant le rendu et avant le chargement des enfants. S’il lève redirect, l’utilisateur part vers /login avec la cible mémorisée dans les search params. Sinon, l’Outlet rend la route demandée, par exemple /admin/users ou /admin/settings. C’est la barrière standard pour un back-office.

Étape 7 — Capturer le reste avec un splat $.tsx

Pour attraper toutes les URLs qui ne matchent aucune route, le router utilise le fichier $.tsx. Ce splat capture le reste du chemin dans la clé _splat. Placé à la racine, il agit comme un fallback 404. Placé dans un sous-dossier, il agit comme un catch-all local, utile pour une documentation où l’arborescence est connue à l’exécution.

// src/routes/$.tsx
import { createFileRoute, useParams } from '@tanstack/react-router'

export const Route = createFileRoute('/$')({
  component: NotFound,
})

function NotFound() {
  const { _splat } = useParams({ from: '/$' })
  return <p>Page introuvable : /{_splat}</p>
}

Tapez une URL inexistante comme /foo/bar/baz : la page affiche Page introuvable : /foo/bar/baz. Le splat doit être déclaré au plus haut niveau de sa zone pour ne pas masquer une route plus spécifique. Les routes plus précises gagnent toujours la priorité sur le splat.

Étape 8 — Grouper sans préfixe avec (parenthèses)

Les parenthèses autour d’un nom de dossier créent un groupe. Le dossier disparaît de l’URL mais regroupe des routes pour l’organisation du code. Par exemple, src/routes/(marketing)/pricing.tsx donne l’URL /pricing, pas /marketing/pricing. C’est utile pour séparer les pages marketing, les pages app et les pages auth dans le repo sans imposer de préfixe public.

src/routes/
  (marketing)/
    pricing.tsx       // => /pricing
    contact.tsx       // => /contact
  (auth)/
    login.tsx         // => /login
    register.tsx      // => /register

Vérification rapide : ouvrez le panneau Devtools, l’arbre n’affiche pas le nom entre parenthèses. Les groupes ne créent pas de layout partagé ; pour partager un layout entre plusieurs routes d’un groupe, combinez le groupe avec un layout pathless, par exemple (auth)/_layout/login.tsx.

Étape 9 — Typer les search params avec Zod

Les query strings sont des entrées non fiables. TanStack Router force la validation à la déclaration via validateSearch. Une fois schéma posé, le hook useSearch retourne un objet typé, et useNavigate refuse à la compilation toute valeur qui sort du schéma. C’est ici que le router devient un vrai filet de sécurité.

// src/routes/search.tsx
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { z } from 'zod'

const searchSchema = z.object({
  q: z.string().optional(),
  page: z.number().int().min(1).default(1),
})

export const Route = createFileRoute('/search')({
  validateSearch: searchSchema,
  component: SearchPage,
})

function SearchPage() {
  const { q, page } = useSearch({ from: '/search' })
  const navigate = useNavigate({ from: '/search' })

  return (
    <div>
      <input
        defaultValue={q ?? ''}
        onChange={(e) =>
          navigate({ search: (prev) => ({ ...prev, q: e.target.value }) })
        }
      />
      <p>Page {page}</p>
    </div>
  )
}

L’URL /search?q=react&page=2 rend la recherche avec la valeur préremplie et la pagination correcte. Si quelqu’un visite /search?page=abc, Zod rejette la valeur et le router applique le défaut 1. Cette discipline empêche les états invalides de remonter dans le composant et supprime la couche de validation manuelle qu’on écrivait avant côté hook.

Conventions de nommage

Le tableau qui suit récapitule les conventions à connaître par cœur. Apprenez-le, et lire un projet TanStack Router devient une question de scan visuel sur l’arborescence.

Nom de fichier Comportement
__root.tsx Racine de l’arbre, contient l’Outlet global et les providers
index.tsx Route exacte du dossier parent
about.tsx Route statique /about
$postId.tsx Paramètre dynamique, accessible via useParams
$.tsx Splat catch-all, capture le reste du chemin dans _splat
_layout.tsx Layout pathless, enveloppe sans ajouter de segment URL
route.tsx Représente la route du dossier qui le contient
(group)/ Groupe d’organisation, n’apparaît pas dans l’URL
posts.index.tsx Notation plate équivalente à posts/index.tsx

La notation plate avec un point est utile pour aplatir un sous-arbre quand on préfère lister les fichiers à la racine de routes/. Le routeur traite posts.$postId.tsx et posts/$postId.tsx de la même manière.

Erreurs fréquentes

Voici les erreurs de débutant qui reviennent en revue de code et leur correction directe. La plupart viennent d’un mauvais ordre dans vite.config.ts ou d’une convention de nommage mal mémorisée.

Symptôme Cause probable Correction
routeTree.gen.ts not found au démarrage Le plugin Vite est déclaré après react() Mettre tanstackRouter en première position du tableau plugins
useParams retourne undefined Nom de fichier sans $ Renommer postId.tsx en $postId.tsx
Layout pathless ignoré Fichier nommé layout.tsx au lieu de _layout.tsx Ajouter l’underscore en préfixe
Search param toujours string au lieu de number Pas de validateSearch ou schéma sans z.number() Brancher Zod et coercer dans le schéma
404 splat masque une vraie route Splat déclaré dans le mauvais dossier Déplacer $.tsx à la racine ou hors du chemin spécifique
Devtools invisibles Build de production Lancer npm run dev, les Devtools se chargent uniquement en mode développement

Un dernier piège : si vous renommez un fichier de route, redémarrez le serveur Vite. Le watcher du plugin n’attrape pas toujours les renommages atomiques sur Windows. Un Ctrl+C suivi d’un npm run dev règle le problème en deux secondes.

FAQ

Les questions ci-dessous reviennent dans les revues de pull request et lors des migrations depuis React Router.

Le fichier routeTree.gen.ts doit-il être versionné ? C’est un artefact, il se régénère à chaque build. La pratique courante est de l’ajouter au .gitignore et de le régénérer en CI avant les tests. Certaines équipes préfèrent le commiter pour avoir des diffs lisibles dans les PR.

Comment obtenir l’URL courante dans un composant ? Utilisez useLocation exporté depuis @tanstack/react-router. Pour un lien typé, préférez le composant Link avec la prop to qui propose l’autocomplétion sur tous les chemins déclarés.

Peut-on combiner file-based et code-based routing ? Oui. Le routeTree.gen.ts est un objet manipulable. Vous pouvez injecter des routes additionnelles via l’API code-based pour des cas dynamiques, mais l’idiome standard reste tout-fichier pour conserver la lisibilité de l’arborescence.

Quelle différence entre _layout.tsx et (group) ? Le layout pathless ajoute un composant qui enveloppe les enfants. Le groupe est une astuce d’organisation des fichiers, sans effet sur l’URL ni sur le rendu. Combinez-les si vous voulez un dossier d’organisation avec un layout commun.

Comment passer des données du loader vers le composant ? Déclarez un loader dans la route, puis appelez Route.useLoaderData() dans le composant. Le typage est inféré automatiquement depuis la signature du loader, sans avoir à dupliquer le type.

Ressources

Pour creuser au-delà de ce socle, voici les références qui font autorité et les autres tutoriels à enchaîner.

Avec ces conventions en tête, vous lirez n’importe quelle base TanStack Router en regardant simplement src/routes. C’est l’objectif : retirer le routage du code applicatif pour le rendre à l’arborescence, et laisser TypeScript faire le filet de sécurité sur les params et les search params.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité