TanStack en 2026 : L'Écosystème Unifié Qui Domine React
Salut HaWkers, si vous travaillez avec React en 2026, vous avez probablement entendu parler de TanStack. Ce qui a commencé comme React Query s'est transformé en un écosystème complet qui change la façon dont nous construisons des applications.
Explorons chaque partie de cet écosystème et comment l'utiliser en pratique.
L'Écosystème TanStack
Vue d'Ensemble en 2026
// Ce que TanStack offre aujourd'hui
const tanstackEcosystem = {
query: {
purpose: 'Server state management',
status: 'Mature et stable',
adoption: 'Standard du marché'
},
router: {
purpose: 'Type-safe routing',
status: 'Alternative sérieuse à React Router',
feature: 'File-based + type safety'
},
table: {
purpose: 'Tables et grilles complexes',
status: 'Leader de la catégorie',
feature: 'Headless, extensible'
},
form: {
purpose: 'Gestion de formulaires',
status: 'En croissance rapide',
feature: 'Type-safe, performant'
},
store: {
purpose: 'Client state management',
status: 'Alternative légère à Redux/Zustand',
feature: 'Basé sur les signals'
},
db: {
purpose: 'Base de données local-first',
status: 'Nouveau en 2025-2026',
feature: 'Sync, offline, temps réel'
},
start: {
purpose: 'Framework full-stack',
status: 'Beta avancée',
feature: 'SSR, file routing, API routes'
},
ai: {
purpose: 'Intégrations AI/LLM',
status: 'Expérimental',
feature: 'Streaming, hooks pour AI'
}
};
TanStack Query
Le Cœur de l'Écosystème
// TanStack Query - Server State Management
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
// Setup
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (anciennement cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Utilisation basique
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery({
queryKey: ['user', userId],
queryFn: async (): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
});
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, isLoading, error } = useUser(userId);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Mutations et Mises à Jour Optimistes
// Mutations avec mises à jour optimistes
interface UpdateUserData {
name?: string;
email?: string;
}
function useUpdateUser(userId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: UpdateUserData) => {
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to update');
return res.json();
},
// Mise à jour optimiste
onMutate: async (newData) => {
// Annule les queries en cours
await queryClient.cancelQueries({ queryKey: ['user', userId] });
// Snapshot de la valeur précédente
const previousUser = queryClient.getQueryData<User>(['user', userId]);
// Met à jour de manière optimiste
queryClient.setQueryData<User>(['user', userId], (old) => ({
...old!,
...newData,
}));
// Retourne le contexte pour rollback
return { previousUser };
},
// En cas d'erreur, rollback
onError: (err, newData, context) => {
if (context?.previousUser) {
queryClient.setQueryData(['user', userId], context.previousUser);
}
},
// Toujours refetch après mutation
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
}
// Utilisation
function EditUserForm({ userId }: { userId: number }) {
const { data: user } = useUser(userId);
const updateUser = useUpdateUser(userId);
const [name, setName] = useState(user?.name ?? '');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateUser.mutate({ name });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={updateUser.isPending}>
{updateUser.isPending ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
</form>
);
}
TanStack Router
Routing Type-Safe
// TanStack Router - Routing avec type safety total
import {
createRootRoute,
createRoute,
createRouter,
RouterProvider,
Link,
Outlet,
} from '@tanstack/react-router';
// Définit la route racine
const rootRoute = createRootRoute({
component: () => (
<div>
<nav>
<Link to="/">Accueil</Link>
<Link to="/users">Utilisateurs</Link>
<Link to="/users/$userId" params={{ userId: '1' }}>
Utilisateur 1
</Link>
</nav>
<Outlet />
</div>
),
});
// Route d'accueil
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>Accueil</h1>,
});
// Route liste des utilisateurs
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: UsersPage,
// Loader pour précharger les données
loader: async () => {
const users = await fetchUsers();
return { users };
},
});
// Route détail utilisateur avec paramètre typé
const userRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetail,
// Validation des paramètres
parseParams: (params) => ({
userId: parseInt(params.userId),
}),
stringifyParams: (params) => ({
userId: String(params.userId),
}),
// Loader avec accès aux params typés
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
// Arbre des routes
const routeTree = rootRoute.addChildren([
homeRoute,
usersRoute.addChildren([userRoute]),
]);
// Instance du router
const router = createRouter({ routeTree });
// Enregistrement des types pour type safety global
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
// App
function App() {
return <RouterProvider router={router} />;
}
// Composants utilisant les données du loader
function UsersPage() {
const { users } = usersRoute.useLoaderData();
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<Link to="/users/$userId" params={{ userId: user.id }}>
{user.name}
</Link>
</li>
))}
</ul>
);
}
function UserDetail() {
const { user } = userRoute.useLoaderData();
const { userId } = userRoute.useParams();
return (
<div>
<h2>{user.name}</h2>
<p>ID : {userId}</p>
</div>
);
}
TanStack Form
Formulaires Type-Safe
// TanStack Form - Gestion de formulaires
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
// Schéma de validation
const userSchema = z.object({
name: z.string().min(2, 'Le nom doit avoir au moins 2 caractères'),
email: z.string().email('Email invalide'),
age: z.number().min(18, 'Doit avoir plus de 18 ans'),
role: z.enum(['admin', 'user', 'guest']),
});
type UserFormData = z.infer<typeof userSchema>;
function UserForm() {
const form = useForm<UserFormData>({
defaultValues: {
name: '',
email: '',
age: 18,
role: 'user',
},
onSubmit: async ({ value }) => {
// value est entièrement typé
await saveUser(value);
},
validators: {
onChange: userSchema,
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="name"
children={(field) => (
<div>
<label>Nom</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.length > 0 && (
<span className="error">
{field.state.meta.errors.join(', ')}
</span>
)}
</div>
)}
/>
<form.Field
name="email"
children={(field) => (
<div>
<label>Email</label>
<input
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.length > 0 && (
<span className="error">
{field.state.meta.errors.join(', ')}
</span>
)}
</div>
)}
/>
<form.Field
name="age"
children={(field) => (
<div>
<label>Âge</label>
<input
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(parseInt(e.target.value))}
/>
</div>
)}
/>
<form.Field
name="role"
children={(field) => (
<div>
<label>Rôle</label>
<select
value={field.state.value}
onChange={(e) =>
field.handleChange(e.target.value as UserFormData['role'])
}
>
<option value="user">Utilisateur</option>
<option value="admin">Admin</option>
<option value="guest">Invité</option>
</select>
</div>
)}
/>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
)}
/>
</form>
);
}
TanStack Table
Tables Complexes
// TanStack Table - Tables headless
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
SortingState,
} from '@tanstack/react-table';
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
}
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
header: 'Nom',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('email', {
header: 'Email',
cell: (info) => (
<a href={`mailto:${info.getValue()}`}>{info.getValue()}</a>
),
}),
columnHelper.accessor('role', {
header: 'Rôle',
cell: (info) => <Badge>{info.getValue()}</Badge>,
}),
columnHelper.accessor('createdAt', {
header: 'Créé le',
cell: (info) => info.getValue().toLocaleDateString(),
sortingFn: 'datetime',
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: (info) => (
<div>
<button onClick={() => editUser(info.row.original)}>Modifier</button>
<button onClick={() => deleteUser(info.row.original.id)}>
Supprimer
</button>
</div>
),
}),
];
function UsersTable({ data }: { data: User[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
{/* Filtre global */}
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Rechercher..."
/>
{/* Table */}
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
style={{ cursor: 'pointer' }}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() === 'asc' && ' 🔼'}
{header.column.getIsSorted() === 'desc' && ' 🔽'}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Précédent
</button>
<span>
Page {table.getState().pagination.pageIndex + 1} sur{' '}
{table.getPageCount()}
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Suivant
</button>
</div>
</div>
);
}
TanStack Store
Gestion d'État Légère
// TanStack Store - État client avec signals
import { Store, useStore } from '@tanstack/store';
// Créer le store
interface AppState {
user: {
id: number;
name: string;
} | null;
theme: 'light' | 'dark';
notifications: string[];
}
const store = new Store<AppState>({
user: null,
theme: 'light',
notifications: [],
});
// Actions comme fonctions
function login(user: AppState['user']) {
store.setState((state) => ({
...state,
user,
}));
}
function logout() {
store.setState((state) => ({
...state,
user: null,
}));
}
function toggleTheme() {
store.setState((state) => ({
...state,
theme: state.theme === 'light' ? 'dark' : 'light',
}));
}
function addNotification(message: string) {
store.setState((state) => ({
...state,
notifications: [...state.notifications, message],
}));
}
// Utilisation dans les composants
function UserMenu() {
// Sélectionne uniquement ce dont on a besoin
const user = useStore(store, (state) => state.user);
if (!user) {
return <button onClick={() => login({ id: 1, name: 'John' })}>Connexion</button>;
}
return (
<div>
<span>Bonjour, {user.name}</span>
<button onClick={logout}>Déconnexion</button>
</div>
);
}
function ThemeToggle() {
const theme = useStore(store, (state) => state.theme);
return (
<button onClick={toggleTheme}>
Thème : {theme === 'light' ? '☀️' : '🌙'}
</button>
);
}
function NotificationBell() {
const count = useStore(store, (state) => state.notifications.length);
return (
<button>
🔔 {count > 0 && <span className="badge">{count}</span>}
</button>
);
}
Intégrer l'Écosystème
Exemple Complet
// App complète utilisant plusieurs outils TanStack
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { Store } from '@tanstack/store';
// Configuration globale
const queryClient = new QueryClient();
const appStore = new Store({ theme: 'light' as const });
// Router avec intégration Query
const rootRoute = createRootRoute({
component: () => {
const theme = useStore(appStore, (s) => s.theme);
return (
<div className={theme}>
<Header />
<Outlet />
</div>
);
},
});
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
// Préchargement avec Query
loader: async ({ context }) => {
await context.queryClient.ensureQueryData({
queryKey: ['users'],
queryFn: fetchUsers,
});
},
component: () => {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <UsersTable data={users ?? []} />;
},
});
// Router avec contexte
const router = createRouter({
routeTree: rootRoute.addChildren([usersRoute]),
context: {
queryClient,
},
});
// App
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}Conclusion
TanStack en 2026 n'est plus "juste React Query" - c'est un écosystème complet et cohérent pour construire des applications React modernes. L'avantage principal est la cohérence : des APIs similaires, du type safety partout, et une intégration parfaite entre les parties.
Quand utiliser chaque partie :
- Query : Chaque fois que vous avez des données serveur
- Router : Si vous voulez du type safety dans le routing (et voulez quitter React Router)
- Table : Tables complexes avec tri/filtre/pagination
- Form : Formulaires moyens à complexes
- Store : État global simple (alternative à Zustand)
L'écosystème continue de grandir, et TanStack Start promet d'être une alternative intéressante à Next.js pour ceux qui veulent un contrôle maximal avec une excellente DX.
Pour en savoir plus sur l'état actuel de React, consultez : React Compiler en 2026.

