Développer un thème WordPress avec les standards 2025
Introduction Le développement de thèmes WordPress a connu une révolution avec l'introduction du Full Site…
Les hooks React sont la fondation des applications modernes. Après avoir maîtrisé useState, il est temps d’explorer useEffect pour les side effects, useContext pour partager des données, et créer vos propres hooks personnalisés.
import { useEffect, useState } from 'react';
// useEffect s'exécute après chaque render
useEffect(() => {
// Code du side effect
console.log('Composant rendu');
});
// Avec tableau de dépendances vide : s'exécute une seule fois (mount)
useEffect(() => {
console.log('Composant monté');
}, []);
// Avec dépendances : s'exécute quand les dépendances changent
useEffect(() => {
console.log('Count a changé:', count);
}, [count]);
// Avec cleanup : s'exécute au démontage
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => {
clearInterval(timer); // Cleanup
};
}, []);
const LifecycleDemo = () => {
const [count, setCount] = useState(0);
// 1. Mount (équivalent componentDidMount)
useEffect(() => {
console.log('✅ Composant monté');
// 4. Unmount (équivalent componentWillUnmount)
return () => {
console.log('❌ Composant démonté');
};
}, []);
// 2. Update (équivalent componentDidUpdate)
useEffect(() => {
console.log('🔄 Count mis à jour:', count);
}, [count]);
// 3. Every render
useEffect(() => {
console.log('🎨 Render complet');
});
return (
Count: {count}
);
};
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = ({ userId }: { userId: number }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset l'état lors du changement d'userId
setLoading(true);
setError(null);
// AbortController pour annuler le fetch si le composant démonte
const controller = new AbortController();
const fetchUser = async () => {
try {
const response = await fetch(/api/users/${userId}, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const data = await response.json();
setUser(data);
} catch (err) {
// Ignorer les erreurs d'abort
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
// Cleanup : annule le fetch si le composant démonte
return () => controller.abort();
}, [userId]); // Re-fetch quand userId change
if (loading) return Chargement...;
if (error) return Erreur: {error};
if (!user) return Utilisateur introuvable;
return (
## {user.name}
{user.email}
);
};
const WindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// Ajouter l'event listener
window.addEventListener('resize', handleResize);
// Cleanup : retirer l'event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Pas de dépendances : s'exécute une seule fois
return (
Largeur: {windowSize.width}px
Hauteur: {windowSize.height}px
);
};
const Timer = () => {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return;
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup : nettoyer l'interval
return () => clearInterval(interval);
}, [isRunning]); // Re-créer l'interval quand isRunning change
return (
## Timer: {seconds}s
);
};
// ❌ ERREUR : Dépendances manquantes
const BadExample = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // count est "stale" après le premier render
}, 1000);
return () => clearInterval(interval);
}, []); // ⚠️ count devrait être dans les dépendances
return {count};
};
// ✅ SOLUTION 1 : Utiliser la fonction de mise à jour
const GoodExample1 = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // Utilise la valeur actuelle
}, 1000);
return () => clearInterval(interval);
}, []); // ✅ Pas besoin de count dans les dépendances
return {count};
};
// ✅ SOLUTION 2 : Ajouter count aux dépendances
const GoodExample2 = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // ✅ count dans les dépendances
return {count};
};
// ❌ ERREUR : Fetch infini
const BadFetch = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}); // ⚠️ Pas de dépendances = s'exécute à chaque render
return {data.length};
};
// ✅ SOLUTION : Dépendances vides
const GoodFetch = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // ✅ S'exécute une seule fois
return {data.length};
};
// theme-context.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// Créer le Context avec une valeur par défaut
const ThemeContext = createContext(undefined);
// Provider component
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(t => t === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
{children}
);
};
// Hook personnalisé pour utiliser le Context
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme doit être utilisé dans un ThemeProvider');
}
return context;
};
// App.tsx
import { ThemeProvider } from './theme-context';
const App = () => {
return (
);
};
// Header.tsx
const Header = () => {
const { theme, toggleTheme } = useTheme();
return (
header-${theme} }>
# Mon App
);
};
// Footer.tsx (profondément imbriqué, mais accède au Context facilement)
const Footer = () => {
const { theme } = useTheme();
return (
);
};
// auth-context.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise;
logout: () => Promise;
isAuthenticated: boolean;
isAdmin: boolean;
}
const AuthContext = createContext(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Vérifier si l'utilisateur est déjà connecté au chargement
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const userData = await response.json();
setUser(userData);
};
const logout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
};
const value = {
user,
loading,
login,
logout,
isAuthenticated: user !== null,
isAdmin: user?.role === 'admin'
};
return (
{children}
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth doit être utilisé dans un AuthProvider');
}
return context;
};
// LoginPage.tsx
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
await login(email, password);
} catch (err) {
setError('Email ou mot de passe incorrect');
}
};
return (
);
};
// ProtectedRoute.tsx
const ProtectedRoute = ({ children }: { children: ReactNode }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return Chargement...;
}
if (!isAuthenticated) {
return ;
}
return <>{children}>;
};
// AdminPanel.tsx
const AdminPanel = () => {
const { isAdmin, user } = useAuth();
if (!isAdmin) {
return Accès refusé;
}
return (
# Panel Admin
Bienvenue {user?.name}
);
};
// App.tsx
const App = () => {
return (
);
};
// Ou avec un helper
const AppProviders = ({ children }: { children: ReactNode }) => {
return (
{children}
);
};
const App = () => {
return (
);
};
function useLocalStorage(key: string, initialValue: T) {
// State pour stocker la valeur
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Fonction pour modifier la valeur
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// Utilisation
const Settings = () => {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
setFontSize(parseInt(e.target.value))}
/>
);
};
interface UseFetchResult {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useFetch(url: string): UseFetchResult {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// Utilisation
interface User {
id: number;
name: string;
}
const UserList = () => {
const { data, loading, error, refetch } = useFetch('/api/users');
if (loading) return Chargement...;
if (error) return Erreur: {error};
return (
{data?.map(user => (
- {user.name}
))}
);
};
function useDebounce(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Utilisation : Recherche avec debounce
const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data, loading } = useFetch(
/api/search?q=${debouncedSearchTerm}
);
return (
setSearchTerm(e.target.value)}
placeholder="Rechercher..."
/>
{loading && Recherche...}
{data && }
);
};
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => {
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handleChange = (e: MediaQueryListEvent) => {
setMatches(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [query]);
return matches;
}
// Utilisation
const ResponsiveLayout = () => {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
{isMobile && }
{isTablet && }
{isDesktop && }
);
};
function useInView(options?: IntersectionObserverInit) {
const [ref, setRef] = useState(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
if (!ref) return;
const observer = new IntersectionObserver(([entry]) => {
setIsInView(entry.isIntersecting);
}, options);
observer.observe(ref);
return () => {
observer.disconnect();
};
}, [ref, options]);
return [setRef, isInView] as const;
}
// Utilisation : Lazy loading d'images
const LazyImage = ({ src, alt }: { src: string; alt: string }) => {
const [ref, isInView] = useInView({ threshold: 0.1 });
return (
{isInView ? (
) : (
Chargement...
)}
);
};
type AsyncState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function useAsync(asyncFunction: () => Promise, immediate = true) {
const [state, setState] = useState>({ status: 'idle' });
const execute = useCallback(async () => {
setState({ status: 'loading' });
try {
const data = await asyncFunction();
setState({ status: 'success', data });
return data;
} catch (error) {
setState({
status: 'error',
error: error instanceof Error ? error : new Error('Unknown error')
});
throw error;
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { ...state, execute };
}
// Utilisation
const UserProfile = ({ userId }: { userId: number }) => {
const fetchUser = useCallback(
() => fetch(/api/users/${userId}).then(res => res.json()),
[userId]
);
const { status, data, error, execute } = useAsync(fetchUser);
if (status === 'loading') return Chargement...;
if (status === 'error') return Erreur: {error.message};
if (status === 'success') {
return (
## {data.name}
);
}
return null;
};
// useFetch.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useFetch } from './useFetch';
describe('useFetch', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
test('retourne les données avec succès', async () => {
const mockData = { id: 1, name: 'Test' };
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockData
});
const { result } = renderHook(() => useFetch('/api/test'));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBeNull();
});
test('gère les erreurs', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(
new Error('Network error')
);
const { result } = renderHook(() => useFetch('/api/test'));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBe('Network error');
expect(result.current.data).toBeNull();
});
});
// useDebounce.test.ts
import { renderHook, act } from '@testing-library/react';
import { useDebounce } from './useDebounce';
jest.useFakeTimers();
describe('useDebounce', () => {
test('debounce la valeur', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
expect(result.current).toBe('initial');
// Changer la valeur
rerender({ value: 'changed', delay: 500 });
// La valeur ne change pas immédiatement
expect(result.current).toBe('initial');
// Avancer le temps
act(() => {
jest.advanceTimersByTime(500);
});
// Maintenant la valeur est mise à jour
expect(result.current).toBe('changed');
});
});
// ❌ Mauvaise pratique
const Parent = () => {
const [count, setCount] = useState(0);
// Cette fonction est recréée à chaque render
const handleClick = () => {
console.log('Clicked');
};
return ;
};
// ✅ Bonne pratique
const Parent = () => {
const [count, setCount] = useState(0);
// Cette fonction est mémorisée
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Dépendances vides = fonction stable
return ;
};
// ❌ Mauvaise pratique : re-render à chaque fois
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState('light');
return (
{children}
);
};
// ✅ Bonne pratique : mémoriser la valeur
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
{children}
);
};
Vous maîtrisez maintenant les hooks avancés de React :
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.