TanStack Router est devenu le routeur le plus rigoureusement typé de l'écosystème React. Contrairement à React Router, où les chaînes de chemins sont typées comme des chaînes et où les paramètres de recherche sont une réflexion après coup, TanStack Router traite chaque route, chaque paramètre et chaque forme d'état de recherche comme un citoyen de première classe en TypeScript. Si vous mal-orthographiez une route, passez un mauvais type de paramètre ou oubliez un paramètre de recherche requis, le compilateur vous arrête avant que le bundle ne soit livré.
Dans ce tutoriel, vous construirez une petite application de tableau de bord réaliste — liste de produits, détail produit, recherche filtrée — en utilisant TanStack Router v1 avec routage basé sur les fichiers, loaders de routes, schémas de paramètres de recherche et UI d'attente. À la fin, vous comprendrez le modèle mental suffisamment pour intégrer le routeur dans une base de code de production.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou plus récent installé
- Une connaissance pratique de React 18 ou 19 et de ses hooks
- Une aisance avec les génériques TypeScript à un niveau débutant à intermédiaire
- Un éditeur avec un bon support TypeScript (VS Code, WebStorm)
- Environ 30 minutes de temps concentré
Vous n'avez pas besoin d'expérience préalable avec TanStack Query, Start ou DB — Router est entièrement utilisable seul.
Ce que vous allez construire
Un tableau de bord à deux pages avec :
- Une page liste des produits sur
/productsavec des paramètres de recherche d'URL typés pour le filtrage - Une page détail produit sur
/products/$productIdavec un loader de données au niveau de la route - Une mise en page imbriquée qui partage une barre latérale entre toutes les routes du tableau de bord
- UI d'attente pilotée par des hooks de style
useNavigationpour un retour instantané pendant les transitions - Composants
Linkentièrement inférés — aucune faute de frappe possible dans les chaînes
Tout fonctionne localement sur Vite. Aucun backend requis.
Étape 1 : Configuration du projet
Créez un nouveau projet Vite React TypeScript et installez les paquets du routeur.
npm create vite@latest tanstack-router-demo -- --template react-ts
cd tanstack-router-demo
npm install
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtoolsLes trois paquets d'intérêt :
@tanstack/react-router— le routeur d'exécution@tanstack/router-plugin— le plugin Vite qui génère l'arbre de routes à partir de la structure de vos fichiers@tanstack/router-devtools— un inspecteur dans le navigateur pour les routes actives, les loaders et les paramètres correspondants
Ouvrez vite.config.ts et branchez le plugin. Le plugin doit s'exécuter avant le plugin React pour que la génération des routes se fasse en premier.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
export default defineConfig({
plugins: [
TanStackRouterVite({ target: "react", autoCodeSplitting: true }),
react(),
],
});autoCodeSplitting: true est le réglage par défaut recommandé en v1 — il découpe paresseusement chaque fichier de route dans son propre chunk sans que vous écriviez un seul import dynamique.
Étape 2 : Définir la route racine
TanStack Router utilise une disposition de fichiers basée sur des conventions sous src/routes/. Créez ce dossier et ajoutez le point d'entrée.
mkdir -p src/routesCréez src/routes/__root.tsx. Le préfixe à double underscore marque le fichier comme la route racine — toutes les autres routes de l'arbre s'imbriquent à l'intérieur.
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
export const Route = createRootRoute({
component: RootLayout,
});
function RootLayout() {
return (
<div className="app">
<header>
<nav>
<Link to="/" activeProps={{ className: "active" }}>
Accueil
</Link>
<Link to="/products" activeProps={{ className: "active" }}>
Produits
</Link>
</nav>
</header>
<main>
<Outlet />
</main>
<TanStackRouterDevtools position="bottom-right" />
</div>
);
}Outlet est l'endroit où les routes enfants sont rendues. Les devtools sont montées une fois à la racine et apparaissent comme un bouton flottant dans le coin.
Étape 3 : Ajouter la route d'index
Créez src/routes/index.tsx pour la page d'accueil.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomePage,
});
function HomePage() {
return (
<section>
<h1>Tableau de bord</h1>
<p>Bienvenue. Visitez la page Produits pour commencer.</p>
</section>
);
}Notez le chemin de route littéral passé à createFileRoute("/"). Il doit correspondre au chemin du fichier sinon le build échouera — le plugin l'impose. Ce littéral est aussi ce qui rend Link to="/" typé : seuls les chemins que le plugin a vus sont valides.
Étape 4 : Initialiser le routeur
Maintenant, branchez l'arbre de routes généré dans le point d'entrée React. Remplacez src/main.tsx :
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);Deux détails importants :
Le fichier routeTree.gen.ts est généré automatiquement par le plugin Vite au premier build ou lancement de dev. Si votre éditeur le signale comme manquant, exécutez npm run dev une fois et il apparaîtra.
Le bloc declare module enregistre votre instance de routeur dans le système de types de TanStack Router. Après cela, des hooks comme useNavigate et useParams connaissent chaque route de votre application, paramètre par paramètre.
Lancez le serveur de dev et confirmez que la page d'accueil se charge.
npm run devÉtape 5 : Construire la liste des produits avec paramètres de recherche
C'est ici que TanStack Router prend de l'avance sur ses concurrents. Les paramètres de recherche d'URL sont typés, validés et analysés automatiquement.
Créez src/routes/products.tsx. Notez qu'il s'agit d'une route de mise en page — elle a des enfants — donc nous utilisons un chemin non-feuille.
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { z } from "zod";
const productSearchSchema = z.object({
category: z.enum(["all", "books", "electronics", "clothing"]).catch("all"),
page: z.number().int().positive().catch(1),
sort: z.enum(["name", "price"]).catch("name"),
});
export const Route = createFileRoute("/products")({
validateSearch: productSearchSchema,
component: ProductsLayout,
});
function ProductsLayout() {
return (
<div className="products-layout">
<aside>
<h2>Filtres</h2>
</aside>
<Outlet />
</div>
);
}Installez Zod si ce n'est pas déjà fait.
npm install zodvalidateSearch s'exécute à chaque navigation. Les appels .catch() fournissent des solutions de repli pour les paramètres manquants ou malformés au lieu de lever une erreur — un petit détail qui économise un temps de débogage énorme une fois que votre application est en production.
Maintenant, créez l'index de la section produits : src/routes/products.index.tsx. La syntaxe à point dans le nom de fichier signifie "la route d'index imbriquée à l'intérieur de /products".
import { createFileRoute, Link } from "@tanstack/react-router";
const allProducts = [
{ id: "p1", name: "Pragmatic Programmer", category: "books", price: 32 },
{ id: "p2", name: "Clavier mécanique", category: "electronics", price: 145 },
{ id: "p3", name: "Sweat à capuche", category: "clothing", price: 65 },
{ id: "p4", name: "Designing Data-Intensive Apps", category: "books", price: 48 },
];
export const Route = createFileRoute("/products/")({
component: ProductsList,
});
function ProductsList() {
const { category, page, sort } = Route.useSearch();
const filtered = allProducts
.filter((p) => category === "all" || p.category === category)
.sort((a, b) => (sort === "price" ? a.price - b.price : a.name.localeCompare(b.name)));
return (
<section>
<header>
<h1>Produits</h1>
<nav className="filter-bar">
<Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "all" })}>
Tous
</Link>
<Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "books" })}>
Livres
</Link>
<Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "electronics" })}>
Électronique
</Link>
</nav>
<p>
Affichage de la page {page}, triée par {sort}
</p>
</header>
<ul>
{filtered.map((p) => (
<li key={p.id}>
<Link to="/products/$productId" params={{ productId: p.id }}>
{p.name} — ${p.price}
</Link>
</li>
))}
</ul>
</section>
);
}Deux modèles à souligner :
Route.useSearch() retourne un objet entièrement typé dérivé du schéma Zod. Pas d'analyse manuelle. Pas d'accès par clé chaîne. Si vous renommez un paramètre de recherche, le compilateur pointe vers chaque consommateur.
La forme fonctionnelle search={(prev) => ({ ...prev, category: "books" })} vous permet de mettre à jour un paramètre de recherche tout en préservant les autres. Plus propre que de concaténer les chaînes de requête à la main.
Étape 6 : Ajouter la route détail produit avec un loader
Créez src/routes/products.$productId.tsx. Le segment $productId marque un paramètre dynamique.
import { createFileRoute, Link, notFound } from "@tanstack/react-router";
type Product = { id: string; name: string; category: string; price: number };
const productDb: Record<string, Product> = {
p1: { id: "p1", name: "Pragmatic Programmer", category: "books", price: 32 },
p2: { id: "p2", name: "Clavier mécanique", category: "electronics", price: 145 },
p3: { id: "p3", name: "Sweat à capuche", category: "clothing", price: 65 },
p4: { id: "p4", name: "Designing Data-Intensive Apps", category: "books", price: 48 },
};
export const Route = createFileRoute("/products/$productId")({
loader: async ({ params }) => {
await new Promise((r) => setTimeout(r, 300));
const product = productDb[params.productId];
if (!product) throw notFound();
return { product };
},
pendingComponent: () => <p>Chargement du produit…</p>,
notFoundComponent: () => <p>Ce produit n'existe pas.</p>,
component: ProductDetail,
});
function ProductDetail() {
const { product } = Route.useLoaderData();
return (
<article>
<Link to="/products" search={{ category: "all", page: 1, sort: "name" }}>
← Retour
</Link>
<h1>{product.name}</h1>
<p>Catégorie : {product.category}</p>
<p>Prix : ${product.price}</p>
</article>
);
}Le loader s'exécute avant que le composant ne soit rendu. Pendant son exécution, pendingComponent est affiché automatiquement — pas besoin de drapeau de chargement manuel. Si le loader lance notFound(), notFoundComponent prend le relais. C'est le même modèle composable que Remix a popularisé, mais avec une inférence TypeScript de bout en bout sur Route.useLoaderData().
Étape 7 : UI d'attente pour les navigations lentes
Par défaut, TanStack Router attend que les loaders se résolvent avant de basculer la vue, ce qui peut sembler lent. Réglez cela avec defaultPendingMs et defaultPreload sur le routeur lui-même. Mettez à jour src/main.tsx :
const router = createRouter({
routeTree,
defaultPreload: "intent",
defaultPreloadStaleTime: 30_000,
defaultPendingMs: 200,
defaultPendingMinMs: 500,
});Ce que fait chacun :
defaultPreload: "intent"commence à charger lors du survol et du focus sur le lien, donc les données sont souvent prêtes au moment où l'utilisateur clique réellementdefaultPreloadStaleTimeempêche les préchargements redondants pendant 30 secondesdefaultPendingMsattend 200ms avant d'afficher l'UI d'attente — les chargements rapides ne font jamais clignoter le spinnerdefaultPendingMinMsgarantit que le spinner reste affiché au moins 500ms une fois affiché, empêchant le scintillement
Ces quatre lignes font la différence entre une application qui semble lente et une qui semble préscient.
Étape 8 : Navigation programmatique
Dans n'importe quel composant, vous pouvez utiliser useNavigate pour la navigation impérative. Le hook est entièrement typé contre l'arbre de routes.
import { useNavigate } from "@tanstack/react-router";
function CheckoutButton({ productId }: { productId: string }) {
const navigate = useNavigate();
return (
<button
onClick={() =>
navigate({
to: "/products/$productId",
params: { productId },
})
}
>
Acheter maintenant
</button>
);
}Essayez de renommer $productId en $slug dans le fichier de route. La trace d'erreurs TypeScript vous mène à chaque appel de navigation qui doit être mis à jour — la refactorisation devient mécanique au lieu d'être risquée.
Tester votre implémentation
Suivez cette liste de contrôle dans le navigateur :
- Ouvrez
/. La page d'accueil se rend avec la barre de navigation. - Cliquez sur Produits. La liste apparaît avec les boutons de filtre.
- Cliquez sur Livres. L'URL devient
/products?category=books&page=1&sort=nameet la liste se filtre. - Modifiez manuellement l'URL en
/products?category=spaceships. La page se rend aveccategory: "all"car le schéma a basculé gracieusement. - Cliquez sur un produit. Notez le bref état d'attente, puis la page de détail.
- Essayez
/products/nonexistentdirectement. Le composant non trouvé se rend. - Ouvrez le panneau devtools (en bas à droite) et inspectez les routes correspondantes, les données du loader et les paramètres de recherche en direct.
Si les huit passent, le routeur est correctement câblé.
Dépannage
routeTree.gen.ts est manquant. Lancez le serveur de dev une fois. Le plugin Vite le génère au premier lancement et le réécrit à chaque sauvegarde de fichier dans src/routes/.
TypeScript pense que chaque route est string. Vous avez oublié le bloc declare module dans main.tsx. Sans lui, le système de types n'a aucune idée que votre routeur existe.
Link to="/foo" ne s'autocomplète pas. Le fichier de route n'a probablement pas été pris en compte. Vérifiez que le fichier vit sous src/routes/, exporte Route, et que le serveur de dev a été redémarré au moins une fois.
Les paramètres de recherche se réinitialisent constamment. Vous avez utilisé la forme objet search={{ category: "books" }} au lieu de la forme fonction search={(prev) => ({ ...prev, category: "books" })}. La forme objet remplace tous les paramètres ; la forme fonction fusionne.
Étapes suivantes
- Associez le routeur à TanStack Query —
loaderdevient un wrapper léger qui appellequeryClient.ensureQueryData()pour une intégration complète du cache - Explorez TanStack Start lorsque vous avez besoin de fonctions serveur, SSR et streaming au-dessus du même routeur
- Lisez la comparaison officielle Code-Based vs File-Based si vous voulez un deuxième avis avant d'engager votre base de code
Conclusion
TanStack Router remplace le routage typé comme chaîne par un arbre vérifié par le compilateur. Les non-correspondances de chemins, les paramètres manquants et les états de recherche malformés deviennent tous des erreurs de build au lieu de bugs d'exécution. Combinez cela avec une UI d'attente intégrée, le préchargement basé sur l'intention et la validation de recherche alimentée par Zod, et vous avez un routeur qui fait vraiment que les applications React se sentent plus rapides — à expédier comme à utiliser.
Le tableau de bord que vous venez de construire est petit, mais les modèles passent à l'échelle. Les mises en page imbriquées se composent. Les schémas de recherche se composent. Les loaders se composent. Intégrez ce routeur dans votre prochaine application React de taille moyenne et les alternatives ne vous manqueront pas.