Optimisation des performances WordPress : Base de données et caching
Introduction Les performances d'un site WordPress dépendent majoritairement de deux facteurs critiques : l'optimisation de…
La gestion d’état est cruciale dans les applications React modernes. En 2025, trois solutions dominent : Redux Toolkit (le standard de l’industrie), Zustand (léger et moderne), et Context API (natif React). Découvrons leurs forces et faiblesses.
// src/contexts/CartContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
items: CartItem[];
addItem: (item: Omit) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
total: number;
itemCount: number;
}
const CartContext = createContext(undefined);
export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState([]);
const addItem = (item: Omit) => {
setItems(current => {
const existing = current.find(i => i.id === item.id);
if (existing) {
return current.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...current, { ...item, quantity: 1 }];
});
};
const removeItem = (id: number) => {
setItems(current => current.filter(i => i.id !== id));
};
const updateQuantity = (id: number, quantity: number) => {
if (quantity <= 0) {
removeItem(id);
return;
}
setItems(current =>
current.map(i => (i.id === id ? { ...i, quantity } : i))
);
};
const total = items.reduce((sum, item) => sum + item.price item.quantity, 0);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const value = {
items,
addItem,
removeItem,
updateQuantity,
total,
itemCount
};
return {children} ;
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
// src/App.tsx
import { CartProvider } from './contexts/CartContext';
function App() {
return (
);
}
// src/components/Header.tsx
import { useCart } from '@/contexts/CartContext';
function Header() {
const { itemCount } = useCart();
return (
# Mon Shop
Panier: {itemCount} articles
);
}
// src/components/ProductCard.tsx
function ProductCard({ product }: { product: Product }) {
const { addItem } = useCart();
return (
### {product.name}
{product.price}€
);
}
// ❌ Problème : tous les consommateurs re-render
const CartContext = createContext({ items: [], addItem: () => {} });
// ✅ Solution 1 : Séparer les contexts
const CartItemsContext = createContext([]);
const CartActionsContext = createContext(undefined);
export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState([]);
const actions = useMemo(() => ({
addItem: (item: CartItem) => setItems(prev => [...prev, item]),
removeItem: (id: number) => setItems(prev => prev.filter(i => i.id !== id))
}), []);
return (
{children}
);
}
// ✅ Solution 2 : useContextSelector (avec use-context-selector)
import { createContext } from 'use-context-selector';
const CartContext = createContext(undefined);
function Header() {
// Ne re-render que si itemCount change
const itemCount = useContextSelector(CartContext, v => v.itemCount);
return Panier: {itemCount};
}
Avantages :
Inconvénients :
npm install zustand
// src/stores/cartStore.ts
import { create } from 'zustand';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
clearCart: () => void;
total: () => number;
itemCount: () => number;
}
export const useCartStore = create((set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(i =>
i.id === id ? { ...i, quantity } : i
)
})),
clearCart: () => set({ items: [] }),
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price item.quantity, 0);
},
itemCount: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.quantity, 0);
}
}));
// src/components/Header.tsx
import { useCartStore } from '@/stores/cartStore';
function Header() {
// Sélecteur : ne re-render que si itemCount change
const itemCount = useCartStore(state => state.itemCount());
return (
# Mon Shop
Panier: {itemCount} articles
);
}
// src/components/ProductCard.tsx
function ProductCard({ product }: { product: Product }) {
// Sélectionner uniquement la fonction
const addItem = useCartStore(state => state.addItem);
return (
### {product.name}
);
}
// src/components/Cart.tsx
function Cart() {
const { items, removeItem, total } = useCartStore(state => ({
items: state.items,
removeItem: state.removeItem,
total: state.total()
}));
return (
{items.map(item => (
{item.name}
{item.quantity}
))}
Total: {total}€
);
}
// src/stores/cartStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export const useCartStore = create()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => ({ / ... / })),
// ... autres actions
}),
{
name: 'cart-storage', // clé localStorage
storage: createJSONStorage(() => localStorage),
}
)
);
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
export const useCartStore = create()(
immer((set) => ({
items: [],
// ✅ Mutations directes grâce à Immer
addItem: (item) => set((state) => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...item, quantity: 1 });
}
}),
updateQuantity: (id, quantity) => set((state) => {
const item = state.items.find(i => i.id === id);
if (item) {
item.quantity = quantity;
}
})
}))
);
import { devtools } from 'zustand/middleware';
export const useCartStore = create()(
devtools(
persist(
immer((set) => ({
// ... store
})),
{ name: 'cart-storage' }
),
{ name: 'CartStore' }
)
);
// src/stores/slices/userSlice.ts
export interface UserSlice {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
export const createUserSlice = (set: any): UserSlice => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null })
});
// src/stores/slices/cartSlice.ts
export const createCartSlice = (set: any): CartSlice => ({
items: [],
addItem: (item) => set((state: any) => ({ / ... / }))
});
// src/stores/index.ts
import { create } from 'zustand';
import { createUserSlice } from './slices/userSlice';
import { createCartSlice } from './slices/cartSlice';
type StoreState = UserSlice & CartSlice;
export const useStore = create()((...a) => ({
...createUserSlice(...a),
...createCartSlice(...a)
}));
Avantages :
Inconvénients :
npm install @reduxjs/toolkit react-redux
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './slices/cartSlice';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
cart: cartReducer,
user: userReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
})
});
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
// src/store/slices/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
loading: boolean;
error: string | null;
}
const initialState: CartState = {
items: [],
loading: false,
error: null
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction>) => {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action: PayloadAction) => {
state.items = state.items.filter(i => i.id !== action.payload);
},
updateQuantity: (state, action: PayloadAction<{ id: number; quantity: number }>) => {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity;
}
},
clearCart: (state) => {
state.items = [];
}
}
});
export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// Selectors
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = (state: RootState) =>
state.cart.items.reduce((sum, item) => sum + item.price item.quantity, 0);
export const selectItemCount = (state: RootState) =>
state.cart.items.reduce((sum, item) => sum + item.quantity, 0);
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = () => useDispatch();
export const useAppSelector: TypedUseSelectorHook = useSelector;
// src/App.tsx
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
);
}
// src/components/Header.tsx
import { useAppSelector } from '@/store/hooks';
import { selectItemCount } from '@/store/slices/cartSlice';
function Header() {
const itemCount = useAppSelector(selectItemCount);
return (
# Mon Shop
Panier: {itemCount} articles
);
}
// src/components/ProductCard.tsx
import { useAppDispatch } from '@/store/hooks';
import { addItem } from '@/store/slices/cartSlice';
function ProductCard({ product }: { product: Product }) {
const dispatch = useAppDispatch();
return (
### {product.name}
);
}
// src/store/slices/productsSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface Product {
id: number;
name: string;
price: number;
}
interface ProductsState {
items: Product[];
loading: boolean;
error: string | null;
}
const initialState: ProductsState = {
items: [],
loading: false,
error: null
};
// Async thunk
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async (category?: string) => {
const url = category
? /api/products?category=${category}
: '/api/products';
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch products');
}
return response.json();
}
);
const productsSlice = createSlice({
name: 'products',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'An error occurred';
});
}
});
export default productsSlice.reducer;
// src/store/api/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface Product {
id: number;
name: string;
price: number;
}
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Products'],
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products',
providesTags: ['Products']
}),
getProduct: builder.query({
query: (id) => products/${id},
providesTags: (result, error, id) => [{ type: 'Products', id }]
}),
createProduct: builder.mutation>({
query: (body) => ({
url: 'products',
method: 'POST',
body
}),
invalidatesTags: ['Products']
}),
updateProduct: builder.mutation({
query: ({ id, ...body }) => ({
url: products/${id},
method: 'PUT',
body
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Products', id }]
}),
deleteProduct: builder.mutation({
query: (id) => ({
url: products/${id},
method: 'DELETE'
}),
invalidatesTags: ['Products']
})
})
});
export const {
useGetProductsQuery,
useGetProductQuery,
useCreateProductMutation,
useUpdateProductMutation,
useDeleteProductMutation
} = productsApi;
// Ajouter à configureStore
export const store = configureStore({
reducer: {
cart: cartReducer,
[productsApi.reducerPath]: productsApi.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productsApi.middleware)
});
// src/components/ProductList.tsx
import {
useGetProductsQuery,
useDeleteProductMutation
} from '@/store/api/productsApi';
function ProductList() {
const { data: products, isLoading, error } = useGetProductsQuery();
const [deleteProduct, { isLoading: isDeleting }] = useDeleteProductMutation();
if (isLoading) return Chargement...;
if (error) return Erreur;
return (
{products?.map(product => (
### {product.name}
))}
);
}
// Préchargement optimiste
function ProductCard({ productId }: { productId: number }) {
const { data: product } = useGetProductQuery(productId);
return {product?.name};
}
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER
} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import cartReducer from './slices/cartSlice';
const persistConfig = {
key: 'root',
storage,
whitelist: ['cart'] // Seulement persister le cart
};
const persistedCartReducer = persistReducer(persistConfig, cartReducer);
export const store = configureStore({
reducer: {
cart: persistedCartReducer,
user: userReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
});
export const persistor = persistStore(store);
// src/App.tsx
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';
function App() {
return (
);
}
Avantages :
Inconvénients :
| Solution | Taille min+gzip | Impact |
|---|---|---|
| Context API | 0kb | Natif React |
| Zustand | 1.1kb | Minimal |
| Redux Toolkit | 12kb | Moyen |
| MobX | 16kb | Élevé |
| Recoil | 14kb | Moyen |
// Benchmark code
function benchmark() {
console.time('Context API');
for (let i = 0; i < 1000; i++) {
// Update context
}
console.timeEnd('Context API'); // ~450ms
console.time('Zustand');
for (let i = 0; i < 1000; i++) {
// Update zustand
}
console.timeEnd('Zustand'); // ~85ms
console.time('Redux Toolkit');
for (let i = 0; i < 1000; i++) {
// Dispatch action
}
console.timeEnd('Redux Toolkit'); // ~120ms
}
| Solution | Temps (ms) | Re-renders |
|---|---|---|
| Context API | 450 | Tous les consommateurs |
| Zustand | 85 | Seulement si sélecteur change |
| Redux Toolkit | 120 | Avec selectors optimisés |
// ✅ Context API : Pour
// - Thème
// - Internationalisation (i18n)
// - Authentification (simple)
// - État UI partagé simple
// ✅ Zustand : Pour
// - Applications petites à moyennes
// - État global simple
// - Performance critique
// - Besoin de simplicité
// ✅ Redux Toolkit : Pour
// - Applications grandes/complexes
// - État global complexe
// - Data fetching centralisé (RTK Query)
// - Besoin de DevTools avancés
// - Middleware custom
// - Time-travel debugging
// Avant (Context API)
const CartContext = createContext(undefined);
export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState([]);
// ...
return {children} ;
}
// Après (Zustand)
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ / ... / }))
}));
// Migration du code
// Avant
const { items, addItem } = useCart();
// Après
const { items, addItem } = useCartStore();
// Ou avec sélecteur
const items = useCartStore(state => state.items);
const addItem = useCartStore(state => state.addItem);
// Avant (Zustand)
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ / ... */ }))
}));
// Après (Redux Toolkit)
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
// ...
}
}
});
// Migration du code
// Avant
const addItem = useCartStore(state => state.addItem);
// Après
const dispatch = useAppDispatch();
dispatch(addItem(product));
// CartContext.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { CartProvider, useCart } from './CartContext';
function TestComponent() {
const { items, addItem } = useCart();
return (
{items.length}
);
}
describe('CartContext', () => {
it('adds items to cart', () => {
render(
);
expect(screen.getByTestId('count')).toHaveTextContent('0');
fireEvent.click(screen.getByText('Add'));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
});
// cartStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCartStore } from './cartStore';
describe('cartStore', () => {
beforeEach(() => {
useCartStore.setState({ items: [] });
});
it('adds item to cart', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: 1, name: 'Test', price: 10 });
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0].name).toBe('Test');
});
});
// cartSlice.test.ts
import cartReducer, { addItem, removeItem } from './cartSlice';
describe('cartSlice', () => {
const initialState = { items: [], loading: false, error: null };
it('adds item', () => {
const item = { id: 1, name: 'Test', price: 10 };
const state = cartReducer(initialState, addItem(item));
expect(state.items).toHaveLength(1);
expect(state.items[0].quantity).toBe(1);
});
it('removes item', () => {
const state = {
items: [{ id: 1, name: 'Test', price: 10, quantity: 1 }],
loading: false,
error: null
};
const newState = cartReducer(state, removeItem(1));
expect(newState.items).toHaveLength(0);
});
});
// Petit projet (< 10 composants)
// → Context API
// Projet moyen (10-50 composants)
// → Zustand
// Grand projet (> 50 composants)
// → Redux Toolkit
// Projet avec beaucoup de data fetching
// → Redux Toolkit + RTK Query
// ou TanStack Query + Zustand
// Context API (auth) + Zustand (cart)
const App = () => (
);
// Redux Toolkit + TanStack Query
// Redux pour l'état global
// TanStack Query pour le data fetching
// Zustand + TanStack Query (moderne et léger)
export const useCartStore = create(...);
export const useProducts = () => useQuery(['products'], fetchProducts);
Chaque solution a ses forces :
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.