AccueilClientsExpertisesOpen SourceBlogContactEstimer

18 juin 2025

TanStack Start: la relève des frameworks React ?

13 minutes de lecture

TanStack Start: la relève des frameworks React ?
🇺🇸 This post is also available in english

Il y a quelques semaines de ça, une faille de sécurité a été découverte dans NextJS au niveau de la gestion de l'authentification au sein du fichier middleware. Cette faille a été corrigée assez rapidement, mais Vercel n'a pas réellement communiqué dessus à vrai dire. Par la suite, les réseaux se sont enflammés et pour la plupart des développeurs, c'était la goutte de trop pour un framework qui souffrait déjà de bien des inconvénients notamment en terme de DX. J'ai pu voir passer de nombreux retours très positifs de la part de développeurs ayant complètement migré de NextJS vers TanStack Start (que nous nommerons Start pour la suite de cet article). Alors au final, est-ce que ce nouveau framework a des chances de concurrencer Remix ou NextJS, déjà bien en place dans l'écosystème React ?

Présentation technique

Start, créé par Tanner Linsley que l'on ne présente plus, possède un routing basé sur une librairie déjà existante, TanStack Router. Utilisable de base pour les SPA, Start l'intègre notamment pour effectuer du SSR, à la manière de ce que fait Remix avec React Router.

Côté bundling, Start se base sur Vite, nous permettant ainsi de bénéficier de tous les plugins Vite disponibles. Pour rappel, Next se base sur SWC ou TurboPack selon les versions.

Explorons plus en détails Start via une petite application toute simple avec une authentification basique, une liste de posts et les détails d'un post.

Exploration

Mise en place

Start propose un exemple de base à cloner faisant office de starter kit. Nous allons ici mettre en place un projet en partant de zéro, utilisant Tailwind CSS.

mkdir starter-example
cd starter-example
yarn init -y

Installons maintenant les dépendances :

yarn add @tanstack/react-start @tanstack/react-router react react-dom zod
yarn add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom tailwindcss @tailwindcss/vite

Mettons en place nos différents fichiers de configuration :

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "moduleResolution": "Bundler",
    "module": "ESNext",
    "target": "ES2022",
    "skipLibCheck": true,
    "strictNullChecks": true,
  },
}
vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";

export default defineConfig({
    plugins: [
      tanstackStart(),
      tailwindcss()
    ],
  },
})

Mettons à jour les scripts de notre package.json :

package.json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}

Créons maintenant notre points d'entrée requis par Start. Celui-ci se situera dans le dossier src à la racine du projet.

src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
    const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    /*
        * il est ici possible de configurer les options par défaut du routeur,
        * comme par exemple le composant par défaut pour les routes en not found,
        */
    })

    return router

}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}

Le fichier routeTree.gen n'existe pas et c'est normal, il est généré automatiquement par le bundler. C'est notamment lui qui est en charge de gérer le typage de nos routes.

Créons notre feuille de style CSS pour Tailwind :

app/styles/styles.css
@import "tailwindcss";

Créons maintenant notre document HTML principal. C'est l'équivalent du fichier layout.tsx à la racine du dossier app d'une application Next. Ce fichier ce situe dans un sous-dossier routes. C'est dans ce sous-dossier que ce situeront toutes les routes de notre application.

src/routes/__root.tsx
import type { ReactNode } from "react";
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router";
// import de la feuille de style au format URL
import appCss from "../styles/styles.css?url";

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: "utf-8",
      },
      {
        name: "viewport",
        content: "width=device-width, initial-scale=1",
      },
      {
        title: "Start Example",
      },
    ],
    links: [
      {
        rel: "stylesheet",
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
});

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  );
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body className="bg-white dark:bg-slate-900">
        <div className="min-h-screen flex flex-col">{children}</div>
        <Scripts />
      </body>
    </html>
  );
}

Le composant Outlet est un composant très utile pour construire des layouts imbriqués. Celui-ci rend le contenu de la route active et peut être utilisé à d'autres endroits de l'application. Nous verrons un cas d'utilisation plus tard.

Nous pouvons maintenant exécuter yarn dev pour démarrer notre bundler.

Notre première route

Créons un fichier src/routes/index.tsx pour notre première route. Si vous avez votre bundler d'ouvert, vous constaterez une fonctionnalité très intéressante venant de TanStack Router : un template de base pour le fichier avec tout le contenu nécessaire est généré. Celui-ci change en fonction du chemin du fichier. Par exemple, renommer le fichier changera le chemin de la route.

src/routes/index.tsx
import { createFileRoute, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: Home,
});

function Home() {
  return (
    <form
      className="flex flex-1 flex-col justify-center items-center gap-4"
    >
      <div className="flex flex-col gap-2">
        <label htmlFor="username" className="dark:text-white">
          Username
        </label>
        <input
          name="username"
          id="username"
          placeholder="Username"
          className="dark:text-white border dark:border-white"
        />
        <label htmlFor="password" className="dark:text-white">
          Password
        </label>
        <input
          name="password"
          id="password"
          placeholder="Password"
          type="password"
          className="dark:text-white border dark:border-white"
        />
      </div>
      <button
        type="submit"
        className="bg-blue-500 px-4 py-3 rounded-md text-white cursor-pointer"
      >
        Login
      </button>
    </form>
  );
}

En se rendant sur http://localhost:3000, notre page formulaire s'affiche correctement !

Mettons maintenant en place notre seconde route, qui sera notre liste de posts. Cette route devra être plus tard protégée par une authentification.

import { createFileRoute } from "@tanstack/react-router";
import PostCard from "../components/PostCard";
import { loadPosts } from "../functions/posts";

export const Route = createFileRoute('/posts')({
  component: RouteComponent,
  loader: () => {
    return loadPosts()
  },
})

function RouteComponent() {
  const loadedPosts = Route.useLoaderData();

  return (
    <div className="flex flex-1 justify-center items-center">
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {loadedPosts.map((post) => (
          <PostCard
            key={post.id}
            title={post.title}
            content={post.content}
            id={post.id}
          />
        ))}
      </div>
    </div>
  );
}

Ici on retrouve une API assez similaire à celle de Remix pour charger de la donnée côté serveur :

  • loader est exécuté au chargement de la page et renvoie notre donnée
  • on récupère cette donnée via le hook useLoaderData accessible depuis notre objet Route.

Ici, notre liste de posts est chargée depuis une fonction serveur, l'opportunité de faire la connaissance avec les server functions de Start.

Server functions

Les server functions sont, comme celles de React et Next, des fonctions appelées depuis n'importe où (sauf les routes API) et exécutées côté serveur. Les server functions de Start ont toutefois des fonctionnalités en plus par rapport à ce que l'on retrouve par exemple sur Next :

  • possibilité de définir la méthode HTTP à utiliser (POST ou GET)
  • ajout de validation (via zod par exemple)
  • accès à l'ensemble de la requête HTTP
  • possibilité de définir le type de réponse (JSON, stream)

Créons notre fonction loadPosts :

import { createServerFn } from "@tanstack/react-start";
import { posts } from "../mocks/posts";

export const loadPosts = createServerFn()
  .handler((async) => {
    return posts;
  });

Profitons-en pour créer notre fonction de login pour notre formulaire ! Chose très intéressante, Start propose une solution clé en main pour gérer les sessions, que ce soit via le session storage ou les cookies.

import { createServerFn, json } from "@tanstack/react-start";
import { setResponseStatus } from "@tanstack/react-start/server";
import { z } from "zod";
import { redirect } from "@tanstack/react-router";
import { setTimeout } from "node:timers/promises";
import { useAppSession } from "../utils/session";

export const loginAction = createServerFn({
  method: "POST",
})
  .validator((data: unknown) => {
    const schema = z.object({
      username: z.string().min(1),
      password: z.string().min(1),
    });

    const result = schema.safeParse(data);

    if (!result.success) {
      throw json(
        { error: result.error.issues, message: "Validation failed" },
        { status: 422 },
      );
    }

    return result.data;
  })
  .handler(async ({ data }) => {
    // Fake delay
    await setTimeout(1000);
    if (data.username !== "admin" && data.password !== "admin") {
      setResponseStatus(401);
      return {
        success: false,
        error: "Unauthorized",
      };
    }

    const session = await useAppSession();

    await session.update({
      name: data.username,
    });

    // On redirige vers la page de posts après la connexion réussie
    throw redirect({
      to: "/posts",
    });
  });

export const logoutAction = createServerFn({ method: "POST" }).handler(
  async () => {
    const session = await useAppSession();

    await session.clear();

    throw redirect({
      to: "/",
    });
  },
);

Et voilà, après la connexion, vous êtes maintenant redirigé vers la liste des posts !

Le concept de layout

Un layout a pour but de créer une structure statique dans laquelle va s'intégrer le contenu de notre route. Avec Next, nous créons un layout via une route layout.tsx au même niveau que la route. Start (via l'intermédiaire du Router) a une approche très similaire à React Router pour gérer les layouts. Tant qu'une partie de notre URL est matchée, alors sa route est rendue.

Par exemple :

  • /posts -> <RootDocument><Posts>
  • /posts/:id -> <RootDocument><Posts><Post>

Ce comportement est configurable en fonction du nom du fichier. Par conséquent, par défaut, une route comme /posts que l'on a créé peut agir comme un layout pour /posts/:id. Le contenu de /posts/:id sera affiché via le composant Outlet.

Il est aussi possible de faire en sorte de créer un layout qui n'est pas lié à une route. C'est ce que l'on appelle un pathless layout. Celui-ci aura un nom de fichier préfixé d'un _.

Créons un layout utilisé au sein de nos routes authentifiées :

src/routes/_app.tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { useServerFn } from "@tanstack/react-start";
import { logoutAction } from "../functions/auth";

export const Route = createFileRoute("/_app")({
  component: RouteComponent,
});

function RouteComponent() {
  const logout = useServerFn(logoutAction);

  return (
    <div className="flex flex-col flex-1">
      <nav className="flex items-center justify-end py-3 px-4">
        <ul>
          <li
            className="dark:text-white hover:underline cursor-pointer"
            onClick={() => {
              logout();
            }}
          >
            Logout
          </li>
        </ul>
      </nav>
      <div className="px-4">
        <Outlet />
      </div>
    </div>
  );
}

Nous devons maintenant déplacer notre route posts.tsx dans un sous dossier _app, correspondant au nom de notre layout. Évidemment, nous pouvons donner le nom que l'on veut à notre layout. Il faut simplement garder une correspondance entre le nom du fichier du layout et le dossier créé.

Notre liste de posts est maintenant bien intégré au sein de notre layout. Il est évidemment possible de nester les layouts à l'infini, c'est le composant Outlet qui se charge de rendre les routes enfant à l'emplacement désiré.

Nous pouvons maintenant, grâce au layout, prévenir l'accès à nos pages authentifiées. En effet, un pathless layout dispose des mêmes possibilités d'usage d'une route. On peut donc utiliser la propriété beforeLoad afin de vérifier la connexion de l'utilisateur.

import type { ReactNode } from "react";
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import appCss from "../styles/styles.css?url";
import { useAppSession } from "../utils/session";

const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
  const session = await useAppSession();

  if (!session.data.name) {
    return null;
  }
  return { name: session.data.name };
});

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: "utf-8",
      },
      {
        name: "viewport",
        content: "width=device-width, initial-scale=1",
      },
      {
        title: "Start Example",
      },
    ],
    links: [
      {
        rel: "stylesheet",
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
  beforeLoad: async () => {
    const user = await fetchUser();

    return { user };
  },
});

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  );
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body className="bg-white dark:bg-slate-900">
        <div className="min-h-screen flex flex-col">{children}</div>
        <Scripts />
      </body>
    </html>
  );
}

Et voilà, maintenant toutes les routes enfant de notre layout _app sont protégées !

Routes API

Comme tout framework full-stack qui se respecte, Start dispose d'un concept de routes API. La notation de fichier pour le routing est la même que pour nos pages classiques.

Créons une route API nous permettant de récupérer notre liste de posts :

src/routes/api/posts/index.ts
import { json } from "@tanstack/react-start";
import { createServerFileRoute } from "@tanstack/react-start/server"
import { posts } from "../../../mocks/posts";

export const APIRoute = createServerFileRoute("/api/posts").methods({
  GET: async ({ request }) => {
    return json(posts);
  },
});

Que ce soit pour les server functions ou les routes API, il est possible d'utiliser tout un ensemble de fonctions utilitaires fournies par Stack afin de gérer à le contenu d'une requête et d'une réponse. C'est le cas de la fonction json. Par exemple si l'on souhaite renvoyer un statut HTTP particulier pour notre requête :

src/routes/api/posts/index.ts
import { json } from "@tanstack/react-start";
import { setResponseStatus, createServerFileRoute } from "@tanstack/react-start/server";
import { posts } from "../../../mocks/posts";

export const APIRoute = createServerFileRoute("/api/posts").methods({
  GET: async ({ request }) => {
    setResponseStatus(200);
    return json(posts);
  },
});

Middlewares

Les middlewares sont des fonctions que l'on branche aux server functions afin d'intercepter l'exécution de celles-ci. Elles permettent d'ajouter du contexte à nos requêtes, que ce soit lors de l'exécution côté client ou côté serveur.

Créons un middleware afin de rediriger l'utilisateur vers la page d'accueil s'il tente d'accéder à notre liste de posts en tant qu'utilisateur anonyme.

import { createMiddleware } from "@tanstack/react-start";
import { redirect } from "@tanstack/react-router";
import { useAppSession } from "../utils/session";

export const authMiddleware = createMiddleware().server(async ({ next }) => {
  const user = await useAppSession();

  if (!user.data.name) {
    throw redirect({
      to: "/",
    });
  }

  return next();
});

Et voilà, maintenant le handler de notre server function ne s'exécutera uniquement si l'utilisateur est authentifié.

Un middleware est aussi utilisable sur une route API :

src/routes/api/posts/index.ts
import { json } from "@tanstack/react-start";
import { setResponseStatus, createServerFileRoute } from "@tanstack/react-start/server";
import { posts } from "../../../mocks/posts";
import { authMiddleware } from "../../../middlewares/auth";

export const APIRoute = createServerFileRoute("/api/posts").methods((api) => ({
  GET: api.middleware([authMiddleware]).handler(async ({ request }) => {
    setResponseStatus(200);
    return json(posts);
  }),
}));

DevTools

Un gros quick win par rapport à Next : la possibilité de débugger les routes (les données du loader par exemple). Cela est disponible via les DevTools de TanStack Router, à intégrer directement au sein du composant RootDocument.

DevTools TanStack Router
DevTools TanStack Router

Déploiement

Le déploiement d'une application Start est très simple. Différents presets sont fournis, à passer soit au sein de la configuration, soit via un flag au moment du build. Pour un simple serveur node par exemple :

yarn build
node .output/server/index.mjs

Start supporte les services d'hébergements les plus importants du marché comme Vercel et Netlify.

Conclusion

Globalement j'ai eu une bonne expérience avec Start. Une DX très agréable, une documentation très complète (comme toutes les librairies TanStack) et toutes les fonctionnalités que l'on attends d'un framework full-stack. Le fonctionnement des server functions est, selon moi, bien mieux que sur NextJS (méthode GET/POST, réponse customisable). L'ajout de devtools liés au routing est aussi un énorme plus. C'est en tout cas une solution sur laquelle nous allons garder un oeil attentif, car c'est une alternative très intéressante par rapport à NextJS. Une idée de projet qui nécessiterait TanStack Start ? Utilisez notre estimateur de projet !

À découvrir également

AI et UI #1 - Filtres intelligents avec le SDK Vercel AI et Next.js

12 Jun 2024

AI et UI #1 - Filtres intelligents avec le SDK Vercel AI et Next.js

Dans ce premier article d’une série consacrée à l’IA et l’UI, je vous propose de découvrir différentes manières d’intégrer ces modèles d’IA dans vos applications React pour améliorer l’expérience utilisateur.

par

Baptiste

Retour d'expérience chez 20 Minutes : s'adapter au journalisme numérique

30 May 2024

Retour d'expérience chez 20 Minutes : s'adapter au journalisme numérique

Dans cet article, vous ne trouverez pas de snippets ou de tips croustillants sur React, seulement le récit d'une aventure plutôt ordinaire à travers le développement et l'évolution d'un projet technique.

par

Vincent

Améliorez vos composants avec Storybook

17 Nov 2020

Améliorez vos composants avec Storybook

Connaissez-vous Storybook ? Cet outil open-source offre un environnement...

par

Baptiste

Premier Octet vous accompagne dans le développement de vos projets avec react

En savoir plusNous contacter
18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
X
Flux RSS
Bluesky

Naviguez à vue