Zustand + Next.js App Router : Gestion d'État React Moderne du Zéro à la Production

Une gestion d'état légère qui fonctionne, tout simplement. Zustand est le gestionnaire d'état minimaliste basé sur les hooks qui est devenu le choix privilégié des développeurs React en 2026. Dans ce tutoriel, vous allez construire une application de panier d'achat réaliste avec Next.js 15 App Router, en apprenant tous les patterns Zustand nécessaires pour la production.
Ce que vous allez apprendre
À la fin de ce tutoriel, vous serez capable de :
- Configurer Zustand dans un projet Next.js 15 App Router avec TypeScript
- Créer des stores type-safe avec des actions, des sélecteurs et des valeurs calculées
- Utiliser les middleware pour le logging, la persistance et l'intégration devtools
- Gérer le rendu côté serveur et l'hydratation avec les stores Zustand
- Implémenter des store slices pour organiser un état complexe
- Construire un état persistant avec
localStorageet des moteurs de stockage personnalisés - Appliquer des patterns concrets pour un panier d'achat en production
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - Une expérience en TypeScript (types, génériques, hooks)
- Une familiarité avec les bases de Next.js App Router (layouts, pages, composants serveur/client)
- Une compréhension de base de l'état React (
useState,useContext)
Pourquoi Zustand en 2026 ?
La gestion d'état React a considérablement évolué. Alors que Redux a dominé pendant des années, l'écosystème s'est orienté vers des solutions plus simples et plus ergonomiques. Voici pourquoi Zustand se démarque :
| Caractéristique | Zustand | Redux Toolkit | Jotai | Context API |
|---|---|---|---|---|
| Taille du bundle | ~1 Ko | ~12 Ko | ~3 Ko | 0 Ko (natif) |
| Boilerplate | Minimal | Modéré | Minimal | Élevé à grande échelle |
| DX TypeScript | Excellente | Bonne | Bonne | Manuelle |
| Support SSR | Natif | Config supplémentaire | Natif | Natif |
| Devtools | Via middleware | Intégré | Extension | Aucun |
| Courbe d'apprentissage | Faible | Modérée | Faible | Faible (monte mal en charge) |
Zustand vous offre la simplicité de useState avec la puissance d'un store global, sans aucun provider requis.
Étape 1 : Configuration du Projet
Créez un nouveau projet Next.js 15 avec TypeScript :
npx create-next-app@latest zustand-cart --typescript --tailwind --app --src-dir
cd zustand-cartInstallez Zustand :
npm install zustandVotre structure de projet devrait ressembler à ceci :
zustand-cart/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── cart/
│ │ └── page.tsx
│ ├── components/
│ ├── stores/
│ └── types/
├── package.json
└── tsconfig.json
Créez les répertoires nécessaires :
mkdir -p src/stores src/types src/componentsÉtape 2 : Définir vos Types
Commencez par des définitions de types propres. Créez src/types/cart.ts :
// src/types/cart.ts
export interface Product {
id: string;
name: string;
price: number;
image: string;
category: string;
}
export interface CartItem {
product: Product;
quantity: number;
}
export interface CartSummary {
totalItems: number;
totalPrice: number;
discount: number;
finalPrice: number;
}Étape 3 : Créer votre Premier Store Zustand
C'est là que Zustand brille. Créez src/stores/cart-store.ts :
// src/stores/cart-store.ts
import { create } from "zustand";
import type { Product, CartItem } from "@/types/cart";
interface CartState {
// État
items: CartItem[];
isOpen: boolean;
// Actions
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
toggleCart: () => void;
// Calculé (état dérivé)
getTotalItems: () => number;
getTotalPrice: () => number;
}
export const useCartStore = create<CartState>((set, get) => ({
// État initial
items: [],
isOpen: false,
// Actions
addItem: (product) =>
set((state) => {
const existingItem = state.items.find(
(item) => item.product.id === product.id
);
if (existingItem) {
return {
items: state.items.map((item) =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return { items: [...state.items, { product, quantity: 1 }] };
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((item) => item.product.id !== productId),
})),
updateQuantity: (productId, quantity) =>
set((state) => ({
items:
quantity <= 0
? state.items.filter((item) => item.product.id !== productId)
: state.items.map((item) =>
item.product.id === productId ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
// Valeurs calculées utilisant get()
getTotalItems: () =>
get().items.reduce((total, item) => total + item.quantity, 0),
getTotalPrice: () =>
get().items.reduce(
(total, item) => total + item.product.price * item.quantity,
0
),
}));Remarquez à quel point c'est propre comparé à Redux. Pas de types d'action, pas de reducers, pas de dispatch. Juste une fonction qui retourne un état et des actions.
Étape 4 : Utiliser le Store dans les Composants
Composant Carte Produit
Créez src/components/product-card.tsx :
// src/components/product-card.tsx
"use client";
import { useCartStore } from "@/stores/cart-store";
import type { Product } from "@/types/cart";
interface ProductCardProps {
product: Product;
}
export function ProductCard({ product }: ProductCardProps) {
const addItem = useCartStore((state) => state.addItem);
return (
<div className="rounded-lg border p-4 shadow-sm hover:shadow-md transition-shadow">
<div className="aspect-square bg-gray-100 rounded-md mb-3 flex items-center justify-center">
<span className="text-4xl">{product.image}</span>
</div>
<h3 className="font-semibold text-lg">{product.name}</h3>
<p className="text-gray-500 text-sm">{product.category}</p>
<div className="flex items-center justify-between mt-3">
<span className="text-xl font-bold">{product.price.toFixed(2)} €</span>
<button
onClick={() => addItem(product)}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
>
Ajouter au panier
</button>
</div>
</div>
);
}Composant Badge Panier
Créez src/components/cart-badge.tsx :
// src/components/cart-badge.tsx
"use client";
import { useCartStore } from "@/stores/cart-store";
export function CartBadge() {
// S'abonner uniquement à ce dont vous avez besoin - c'est la clé de la performance
const totalItems = useCartStore((state) => state.getTotalItems());
const toggleCart = useCartStore((state) => state.toggleCart);
return (
<button
onClick={toggleCart}
className="relative p-2 rounded-full hover:bg-gray-100"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z"
/>
</svg>
{totalItems > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{totalItems}
</span>
)}
</button>
);
}Point clé : Remarquez comment nous utilisons (state) => state.getTotalItems() comme sélecteur. Zustand ne re-rend un composant que lorsque la valeur sélectionnée change. C'est une optimisation de performance automatique.
Étape 5 : Sélecteurs et Performance
Zustand ne re-rend les composants que lorsque l'état sélectionné change. C'est crucial pour la performance. Voici les patterns :
Mauvais : Sélectionner tout le store
// Ceci re-rend à CHAQUE changement d'état
const store = useCartStore();Bon : Sélectionner des valeurs spécifiques
// Ne re-rend que quand items change
const items = useCartStore((state) => state.items);
// Ne re-rend que quand isOpen change
const isOpen = useCartStore((state) => state.isOpen);Avancé : Fonctions d'égalité personnalisées
Pour les valeurs dérivées qui retournent de nouvelles références d'objet, utilisez la comparaison shallow :
import { useShallow } from "zustand/react/shallow";
// Ne re-rend que quand les valeurs réelles changent, pas la référence
const { items, isOpen } = useCartStore(
useShallow((state) => ({
items: state.items,
isOpen: state.isOpen,
}))
);Créer des Sélecteurs Réutilisables
Créez src/stores/cart-selectors.ts :
// src/stores/cart-selectors.ts
import type { CartState } from "@/stores/cart-store";
export const selectCartItems = (state: CartState) => state.items;
export const selectIsCartOpen = (state: CartState) => state.isOpen;
export const selectCartTotal = (state: CartState) => state.getTotalPrice();
export const selectCartCount = (state: CartState) => state.getTotalItems();
export const selectItemById = (productId: string) => (state: CartState) =>
state.items.find((item) => item.product.id === productId);Utilisation :
const items = useCartStore(selectCartItems);
const total = useCartStore(selectCartTotal);
const item = useCartStore(selectItemById("product-1"));Étape 6 : Middleware - Persistance
L'une des fonctionnalités les plus puissantes de Zustand est son système de middleware. Ajoutons la persistance pour sauvegarder le panier dans localStorage.
Mettez à jour src/stores/cart-store.ts :
// src/stores/cart-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import type { Product, CartItem } from "@/types/cart";
interface CartState {
items: CartItem[];
isOpen: boolean;
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
toggleCart: () => void;
getTotalItems: () => number;
getTotalPrice: () => number;
}
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
isOpen: false,
addItem: (product) =>
set((state) => {
const existingItem = state.items.find(
(item) => item.product.id === product.id
);
if (existingItem) {
return {
items: state.items.map((item) =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return { items: [...state.items, { product, quantity: 1 }] };
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((item) => item.product.id !== productId),
})),
updateQuantity: (productId, quantity) =>
set((state) => ({
items:
quantity <= 0
? state.items.filter((item) => item.product.id !== productId)
: state.items.map((item) =>
item.product.id === productId ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
getTotalItems: () =>
get().items.reduce((total, item) => total + item.quantity, 0),
getTotalPrice: () =>
get().items.reduce(
(total, item) => total + item.product.price * item.quantity,
0
),
}),
{
name: "cart-storage",
storage: createJSONStorage(() => localStorage),
// Persister uniquement les données, pas l'état UI comme isOpen
partialize: (state) => ({ items: state.items }),
}
)
);L'option partialize est importante : vous ne devriez persister que les données, pas l'état de l'interface comme l'ouverture du tiroir du panier.
Étape 7 : Gérer le SSR et l'Hydratation
Next.js App Router effectue d'abord le rendu des composants côté serveur. Comme localStorage n'existe pas côté serveur, nous devons gérer l'hydratation avec soin.
Créez src/stores/hydration.ts :
// src/stores/hydration.ts
"use client";
import { useEffect, useState } from "react";
export function useHydration() {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true);
}, []);
return hydrated;
}Créez un composant de panier sécurisé pour l'hydratation dans src/components/cart-sidebar.tsx :
// src/components/cart-sidebar.tsx
"use client";
import { useCartStore } from "@/stores/cart-store";
import { useHydration } from "@/stores/hydration";
export function CartSidebar() {
const hydrated = useHydration();
const items = useCartStore((state) => state.items);
const isOpen = useCartStore((state) => state.isOpen);
const toggleCart = useCartStore((state) => state.toggleCart);
const removeItem = useCartStore((state) => state.removeItem);
const updateQuantity = useCartStore((state) => state.updateQuantity);
const clearCart = useCartStore((state) => state.clearCart);
const getTotalPrice = useCartStore((state) => state.getTotalPrice);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex justify-end">
{/* Fond sombre */}
<div className="absolute inset-0 bg-black/50" onClick={toggleCart} />
{/* Barre latérale */}
<div className="relative w-full max-w-md bg-white shadow-xl p-6 overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">Votre Panier</h2>
<button onClick={toggleCart} className="text-gray-500 hover:text-gray-700">
✕
</button>
</div>
{!hydrated ? (
<div className="animate-pulse space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-20 bg-gray-200 rounded" />
))}
</div>
) : items.length === 0 ? (
<p className="text-gray-500 text-center py-8">Votre panier est vide</p>
) : (
<>
<div className="space-y-4">
{items.map((item) => (
<div
key={item.product.id}
className="flex items-center gap-4 border-b pb-4"
>
<span className="text-3xl">{item.product.image}</span>
<div className="flex-1">
<h3 className="font-medium">{item.product.name}</h3>
<p className="text-gray-500">
{item.product.price.toFixed(2)} €
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() =>
updateQuantity(item.product.id, item.quantity - 1)
}
className="w-8 h-8 rounded-full border flex items-center justify-center"
>
-
</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() =>
updateQuantity(item.product.id, item.quantity + 1)
}
className="w-8 h-8 rounded-full border flex items-center justify-center"
>
+
</button>
</div>
<button
onClick={() => removeItem(item.product.id)}
className="text-red-500 hover:text-red-700"
>
✕
</button>
</div>
))}
</div>
<div className="mt-6 border-t pt-4">
<div className="flex justify-between text-xl font-bold">
<span>Total :</span>
<span>{getTotalPrice().toFixed(2)} €</span>
</div>
<button className="w-full mt-4 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors">
Passer à la caisse
</button>
<button
onClick={clearCart}
className="w-full mt-2 text-gray-500 hover:text-gray-700"
>
Vider le panier
</button>
</div>
</>
)}
</div>
</div>
);
}Étape 8 : Store Slices pour les Applications Complexes
Au fur et à mesure que votre application grandit, vous voudrez diviser votre store en slices. Cela garde chaque partie focalisée et testable.
Créez src/stores/slices/cart-slice.ts :
// src/stores/slices/cart-slice.ts
import type { StateCreator } from "zustand";
import type { Product, CartItem } from "@/types/cart";
export interface CartSlice {
items: CartItem[];
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
clearCart: () => void;
getTotalItems: () => number;
getTotalPrice: () => number;
}
export const createCartSlice: StateCreator<CartSlice> = (set, get) => ({
items: [],
addItem: (product) =>
set((state) => {
const existing = state.items.find((i) => i.product.id === product.id);
if (existing) {
return {
items: state.items.map((i) =>
i.product.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
),
};
}
return { items: [...state.items, { product, quantity: 1 }] };
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((i) => i.product.id !== productId),
})),
clearCart: () => set({ items: [] }),
getTotalItems: () =>
get().items.reduce((sum, i) => sum + i.quantity, 0),
getTotalPrice: () =>
get().items.reduce((sum, i) => sum + i.product.price * i.quantity, 0),
});Créez src/stores/slices/ui-slice.ts :
// src/stores/slices/ui-slice.ts
import type { StateCreator } from "zustand";
export interface UISlice {
isCartOpen: boolean;
isSearchOpen: boolean;
theme: "light" | "dark";
toggleCart: () => void;
toggleSearch: () => void;
setTheme: (theme: "light" | "dark") => void;
}
export const createUISlice: StateCreator<UISlice> = (set) => ({
isCartOpen: false,
isSearchOpen: false,
theme: "light",
toggleCart: () => set((state) => ({ isCartOpen: !state.isCartOpen })),
toggleSearch: () => set((state) => ({ isSearchOpen: !state.isSearchOpen })),
setTheme: (theme) => set({ theme }),
});Combinez les slices dans src/stores/app-store.ts :
// src/stores/app-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
type AppStore = CartSlice & UISlice;
export const useAppStore = create<AppStore>()(
persist(
(...args) => ({
...createCartSlice(...args),
...createUISlice(...args),
}),
{
name: "app-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ items: state.items, theme: state.theme }),
}
)
);Vous avez maintenant un store propre et modulaire qui évolue avec votre application.
Étape 9 : Devtools et Middleware de Logging
Empilez plusieurs middleware pour une excellente expérience de développement :
// src/stores/app-store.ts (version améliorée)
import { create } from "zustand";
import { persist, createJSONStorage, devtools } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
type AppStore = CartSlice & UISlice;
export const useAppStore = create<AppStore>()(
devtools(
persist(
(...args) => ({
...createCartSlice(...args),
...createUISlice(...args),
}),
{
name: "app-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ items: state.items, theme: state.theme }),
}
),
{ name: "AppStore" }
)
);Installez l'extension navigateur Redux DevTools pour inspecter votre store Zustand en temps réel. Chaque changement d'état apparaîtra avec les noms des actions et les diffs d'état.
Middleware de Logging Personnalisé
// src/stores/middleware/logger.ts
import type { StateCreator, StoreMutatorIdentifier } from "zustand";
type Logger = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>,
name?: string
) => StateCreator<T, Mps, Mcs>;
type LoggerImpl = <T>(
f: StateCreator<T, [], []>,
name?: string
) => StateCreator<T, [], []>;
const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...args) => {
const prev = get();
set(...(args as Parameters<typeof set>));
const next = get();
if (process.env.NODE_ENV === "development") {
console.log(`[${name ?? "store"}]`, { prev, next });
}
};
return f(loggedSet, get, store);
};
export const logger = loggerImpl as unknown as Logger;Étape 10 : Tout Assembler
Créez la page principale dans src/app/page.tsx :
// src/app/page.tsx
import { ProductCard } from "@/components/product-card";
import { CartBadge } from "@/components/cart-badge";
import { CartSidebar } from "@/components/cart-sidebar";
import type { Product } from "@/types/cart";
const products: Product[] = [
{ id: "1", name: "Casque Sans Fil", price: 79.99, image: "🎧", category: "Électronique" },
{ id: "2", name: "Clavier Mécanique", price: 149.99, image: "⌨️", category: "Électronique" },
{ id: "3", name: "Chaussures de Course", price: 129.99, image: "👟", category: "Sport" },
{ id: "4", name: "Machine à Café", price: 89.99, image: "☕", category: "Cuisine" },
{ id: "5", name: "Lampe de Bureau", price: 39.99, image: "💡", category: "Bureau" },
{ id: "6", name: "Sac à Dos", price: 59.99, image: "🎒", category: "Accessoires" },
];
export default function HomePage() {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">Zustand Store</h1>
<CartBadge />
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">
<h2 className="text-3xl font-bold mb-8">Produits</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</main>
<CartSidebar />
</div>
);
}Étape 11 : Tester les Stores Zustand
Les stores Zustand sont de simples fonctions, ce qui les rend faciles à tester :
// src/stores/__tests__/cart-store.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useCartStore } from "../cart-store";
const mockProduct = {
id: "test-1",
name: "Produit Test",
price: 29.99,
image: "🧪",
category: "Test",
};
describe("Cart Store", () => {
beforeEach(() => {
// Réinitialiser le store entre les tests
useCartStore.setState({ items: [] });
});
it("ajoute un article au panier", () => {
useCartStore.getState().addItem(mockProduct);
const items = useCartStore.getState().items;
expect(items).toHaveLength(1);
expect(items[0].product.id).toBe("test-1");
expect(items[0].quantity).toBe(1);
});
it("incrémente la quantité pour les articles existants", () => {
useCartStore.getState().addItem(mockProduct);
useCartStore.getState().addItem(mockProduct);
const items = useCartStore.getState().items;
expect(items).toHaveLength(1);
expect(items[0].quantity).toBe(2);
});
it("supprime un article du panier", () => {
useCartStore.getState().addItem(mockProduct);
useCartStore.getState().removeItem("test-1");
expect(useCartStore.getState().items).toHaveLength(0);
});
it("calcule le prix total correctement", () => {
useCartStore.getState().addItem(mockProduct);
useCartStore.getState().addItem(mockProduct);
expect(useCartStore.getState().getTotalPrice()).toBe(59.98);
});
it("vide entièrement le panier", () => {
useCartStore.getState().addItem(mockProduct);
useCartStore.getState().clearCart();
expect(useCartStore.getState().items).toHaveLength(0);
});
});Remarquez comment vous accédez directement au store avec getState() et setState() en dehors des composants React. Pas besoin d'utilitaires de rendu ni de providers.
Dépannage
Avertissements de désynchronisation d'hydratation
Si vous voyez des désynchronisations d'hydratation avec les stores persistants, assurez-vous que les composants client utilisant un état persistant affichent un squelette de chargement jusqu'à l'hydratation :
const hydrated = useHydration();
if (!hydrated) return <Skeleton />;Le store ne se met pas à jour entre les onglets
Ajoutez l'écouteur d'événement storage pour la synchronisation entre onglets :
persist(
// ... configuration du store
{
name: "cart-storage",
storage: createJSONStorage(() => localStorage),
// Ceci active la synchronisation entre onglets
skipHydration: false,
}
)Problèmes de performance avec les grands stores
Utilisez des sélecteurs granulaires au lieu de sélectionner le store entier. Utilisez useShallow lorsque vous sélectionnez plusieurs valeurs qui retournent de nouvelles références d'objet.
Prochaines Étapes
Maintenant que vous maîtrisez Zustand avec Next.js App Router, voici quelques pistes pour aller plus loin :
- Ajoutez le middleware Immer pour des mises à jour d'état imbriqué plus ergonomiques
- Construisez une wishlist en utilisant un slice de store séparé
- Intégrez une vraie API en utilisant les actions asynchrones de Zustand
- Ajoutez des mises à jour optimistes pour les mutations serveur
- Explorez
subscribeWithSelectorde Zustand pour les effets de bord hors React
Conclusion
Zustand offre l'équilibre parfait entre simplicité et puissance pour la gestion d'état React en 2026. Son API minimale, son excellent support TypeScript et son intégration transparente avec Next.js App Router en font le choix idéal pour les applications modernes.
Vous avez appris à créer des stores, utiliser des sélecteurs pour la performance, ajouter la persistance, gérer l'hydratation SSR, organiser le code avec des slices et tout tester. Ces patterns fonctionnent aussi bien pour de simples applications compteur que pour des plateformes e-commerce complexes.
Le point clé à retenir : commencez simplement avec un seul store, ajoutez des middleware selon les besoins, et divisez en slices uniquement quand la complexité l'exige. Zustand évolue avec votre application sans jamais vous gêner.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Chatbot IA Local avec Ollama et Next.js : Guide Complet
Construisez un chatbot IA privé fonctionnant entièrement sur votre machine locale avec Ollama et Next.js. Ce tutoriel pratique couvre l'installation, le streaming des réponses, la sélection de modèles et le déploiement d'une interface de chat prête pour la production — le tout sans envoyer de données dans le cloud.

Construire un site web axé sur le contenu avec Payload CMS 3 et Next.js
Apprenez à construire un site web complet avec Payload CMS 3, qui fonctionne nativement dans Next.js App Router. Ce tutoriel couvre les collections, le rich text, les uploads, l'authentification et le déploiement en production.