React 18+ : Hooks, Context, et Patterns modernes
Installation et configuration
Création d’un projet
Create React App
npx create-react-app mon-app
npx create-react-app mon-app --template typescript
Vite (recommandé pour performance)
npm create vite@latest mon-app -- --template react
npm create vite@latest mon-app -- --template react-ts
Next.js (avec SSR)
npx create-next-app@latest mon-app
Installation dans projet existant
npm install react react-dom
npm install --save-dev @types/react @types/react-dom # TypeScript
Structure de projet moderne
src/
├── components/
│ ├── common/ # Composants réutilisables
│ ├── features/ # Composants par fonctionnalité
│ └── layout/ # Layout components
├── hooks/ # Custom hooks
├── contexts/ # Context providers
├── services/ # API calls, services
├── utils/ # Fonctions utilitaires
├── types/ # Types TypeScript
├── constants/ # Constantes
└── App.tsx
Composants fonctionnels
Syntaxe de base
// Composant simple
function Welcome() {
return Bonjour
;
}
// Avec props
function Welcome({ name }) {
return Bonjour {name}
;
}
// Arrow function
const Welcome = ({ name }) => {
return Bonjour {name}
;
};
// Return implicite
const Welcome = ({ name }) => Bonjour {name}
;
// Avec destructuration
const UserCard = ({ user: { name, email, age } }) => (
{name}
{email}
{age} ans
);
// Props par défaut
function Button({ text = 'Cliquer', type = 'button' }) {
return ;
}
// Rest props
function Input({ label, ...inputProps }) {
return (
);
}
TypeScript avec React
// Props interface
interface ButtonProps {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button: React.FC = ({
text,
onClick,
variant = 'primary',
disabled = false
}) => (
);
// Avec children
interface CardProps {
title: string;
children: React.ReactNode;
}
const Card: React.FC = ({ title, children }) => (
{title}
{children}
);
// Props avec génériques
interface ListProps {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List({ items, renderItem }: ListProps) {
return (
{items.map((item, index) => (
- {renderItem(item)}
))}
);
}
// Event handlers
interface FormProps {
onSubmit: (data: FormData) => void;
}
const Form: React.FC = ({ onSubmit }) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// ...
};
const handleChange = (e: React.ChangeEvent) => {
// ...
};
return ;
};
Hooks essentiels
useState
import { useState } from 'react';
// État simple
function Counter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
// Fonction de mise à jour (callback)
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prevCount => prevCount + 1); // Plus sûr
};
const incrementTwice = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Fonctionne correctement
};
return ;
}
// État objet
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleChange = (e) => {
setUser({
...user,
[e.target.name]: e.target.value
});
};
return (
);
}
// État tableau
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
{todos.map(todo => (
toggleTodo(todo.id)}
>
{todo.text}
))}
);
}
// Lazy initial state (pour calculs coûteux)
function ExpensiveComponent() {
const [data, setData] = useState(() => {
const initialData = computeExpensiveValue();
return initialData;
});
return {data};
}
// État avec TypeScript
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
const [items, setItems] = useState([]);
useEffect
import { useEffect, useState } from 'react';
// Effect de base (s'exécute après chaque render)
useEffect(() => {
console.log('Composant rendu');
});
// Effect avec cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(timer); // Cleanup
});
// Effect avec dépendances (s'exécute quand dépendances changent)
useEffect(() => {
console.log('Count a changé:', count);
}, [count]);
// Effect une seule fois (mount)
useEffect(() => {
console.log('Composant monté');
return () => {
console.log('Composant démonté');
};
}, []); // Tableau vide
// Fetch de données
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(/api/users/${userId});
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchUser();
return () => {
cancelled = true; // Évite setState sur composant démonté
};
}, [userId]);
if (loading) return Chargement...;
if (error) return Erreur: {error};
return {user?.name};
}
// Synchronisation avec localStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Event listeners
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return {size.width} x {size.height};
}
// Multiples effects (séparation des responsabilités)
function Profile({ userId }) {
useEffect(() => {
// Effect 1: Fetch user data
fetchUser(userId);
}, [userId]);
useEffect(() => {
// Effect 2: Track analytics
trackPageView('profile', userId);
}, [userId]);
useEffect(() => {
// Effect 3: Subscribe to updates
const unsubscribe = subscribeToUserUpdates(userId);
return unsubscribe;
}, [userId]);
}
useContext
import { createContext, useContext, useState } from 'react';
// Création du contexte
const ThemeContext = createContext();
// Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
{children}
);
}
// Hook personnalisé pour utiliser le contexte
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Utilisation
function App() {
return (
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
);
}
// Context avec TypeScript
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const userData = await response.json();
setUser(userData);
};
const logout = () => {
setUser(null);
};
const value = {
user,
login,
logout,
isAuthenticated: !!user
};
return (
{children}
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Multiple contexts
function App() {
return (
);
}
useRef
import { useRef, useEffect } from 'react';
// Référence à un élément DOM
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
);
}
// Auto-focus au mount
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return ;
}
// Stocker valeur mutable (ne déclenche pas re-render)
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (!intervalRef.current) {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => stopTimer(); // Cleanup
}, []);
return (
{count}
);
}
// Valeur précédente
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Utilisation
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
Current: {count}
Previous: {prevCount}
);
}
// Mesurer dimensions d'un élément
function useElementSize() {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const element = ref.current;
if (!element) return;
const resizeObserver = new ResizeObserver(entries => {
const { width, height } = entries[0].contentRect;
setSize({ width, height });
});
resizeObserver.observe(element);
return () => resizeObserver.disconnect();
}, []);
return [ref, size];
}
// Forward ref (passer ref à composant enfant)
import { forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => (
));
function Parent() {
const inputRef = useRef();
return (
);
}
// TypeScript avec useRef
const inputRef = useRef(null);
const divRef = useRef(null);
const timerRef = useRef(null);
useReducer
import { useReducer } from 'react';
// Reducer simple
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error(Unknown action: ${action.type});
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
Count: {state.count}
);
}
// Reducer complexe avec payload
const initialState = {
todos: [],
filter: 'all'
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADDTODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, done: false }
]
};
case 'TOGGLETODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, done: !todo.done }
: todo
)
};
case 'DELETETODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'SETFILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({ type: 'ADDTODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLETODO', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETETODO', payload: id });
};
return (
{/ UI /}
);
}
// TypeScript avec useReducer
type State = {
count: number;
error: string | null;
};
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'seterror'; payload: string }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'seterror':
return { ...state, error: action.payload };
case 'reset':
return { count: 0, error: null };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
// Lazy initialization
function init(initialCount) {
return { count: initialCount };
}
const [state, dispatch] = useReducer(reducer, 0, init);
useMemo
import { useMemo, useState } from 'react';
// Calcul coûteux mémorisé
function ExpensiveComponent({ items }) {
const sortedItems = useMemo(() => {
console.log('Tri des items...');
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // Recalcule seulement si items change
return (
{sortedItems.map(item => (
- {item.name}
))}
);
}
// Filtrage et tri
function ProductList({ products, searchTerm, sortBy }) {
const processedProducts = useMemo(() => {
let filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [products, searchTerm, sortBy]);
return (
{processedProducts.map(product => (
))}
);
}
// Éviter création d'objet à chaque render
function UserProfile({ userId }) {
const [data, setData] = useState(null);
const requestConfig = useMemo(() => ({
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${token}
}
}), [token]);
useEffect(() => {
fetch(/api/users/${userId}, requestConfig)
.then(res => res.json())
.then(setData);
}, [userId, requestConfig]);
return {data?.name};
}
// Contexte value mémorisé
function MyProvider({ children }) {
const [state, setState] = useState(initialState);
const value = useMemo(() => ({
state,
setState,
// autres fonctions...
}), [state]);
return (
{children}
);
}
// Note: N'utilisez useMemo que pour optimisations mesurées
// Pas besoin pour calculs simples
useCallback
import { useCallback, useState, memo } from 'react';
// Fonction mémorisée
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Sans useCallback, nouvelle fonction à chaque render
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Fonction stable, ne change jamais
const handleSubmit = useCallback((data) => {
console.log('Submitting:', data);
}, []);
return (
Count: {count}
);
}
// Composant enfant mémorisé (ne re-render que si props changent)
const Child = memo(({ onIncrement }) => {
console.log('Child rendered');
return ;
});
// Avec dépendances
function SearchComponent() {
const [query, setQuery] = useState('');
const [filters, setFilters] = useState({});
const search = useCallback((searchQuery) => {
// Utilise filters dans la closure
console.log('Searching:', searchQuery, 'with filters:', filters);
}, [filters]); // Recréée si filters change
return (
);
}
// Event handlers
function Form() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}, []); // Pas de dépendances, utilise setter avec callback
const handleSubmit = useCallback((e) => {
e.preventDefault();
console.log('Submit:', formData);
}, [formData]);
return (
);
}
// TypeScript
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []);
const handleChange = useCallback(
(e: React.ChangeEvent) => {
console.log(e.target.value);
},
[]
);
useLayoutEffect
import { useLayoutEffect, useRef, useState } from 'react';
// Mesurer élément avant affichage
function Tooltip({ children, text }) {
const [coords, setCoords] = useState({ x: 0, y: 0 });
const tooltipRef = useRef(null);
const buttonRef = useRef(null);
useLayoutEffect(() => {
const button = buttonRef.current;
const tooltip = tooltipRef.current;
if (button && tooltip) {
const buttonRect = button.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
setCoords({
x: buttonRect.left + (buttonRect.width - tooltipRect.width) / 2,
y: buttonRect.top - tooltipRect.height - 10
});
}
}, [text]);
return (
<>
{text}
>
);
}
// Animation avant render
function AnimatedComponent() {
const divRef = useRef(null);
useLayoutEffect(() => {
// S'exécute de manière synchrone avant paint
const div = divRef.current;
if (div) {
div.style.transform = 'translateX(0)';
}
}, []);
return ;
}
// Note: Préférez useEffect sauf si vous devez mesurer/modifier DOM
// avant affichage pour éviter flash visuel
Hooks React 18+
useTransition
import { useTransition, useState } from 'react';
// Marquer mise à jour comme non-urgente
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// Mise à jour urgente (input)
setQuery(e.target.value);
// Mise à jour non-urgente (résultats)
startTransition(() => {
const filtered = searchData(e.target.value);
setResults(filtered);
});
};
return (
{isPending && Recherche...}
);
}
// Tabs avec transition
function Tabs() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const selectTab = (nextTab) => {
startTransition(() => {
setTab(nextTab);
});
};
return (
{isPending && }
{tab === 'home' && }
{tab === 'profile' && }
{tab === 'settings' && }
);
}
useDeferredValue
import { useDeferredValue, useState, memo } from 'react';
// Différer valeur pour optimiser performance
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
setQuery(e.target.value)}
/>
);
}
// SearchResults ne re-render pas à chaque frappe
const SearchResults = memo(({ query }) => {
const results = useMemo(() => {
return searchDatabase(query); // Opération coûteuse
}, [query]);
return (
{results.map(result => (
- {result.name}
))}
);
});
// Avec indication de chargement
function ProductFilter() {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
const isStale = filter !== deferredFilter;
return (
setFilter(e.target.value)} />
);
}
useId
import { useId } from 'react';
// ID unique pour accessibilité
function FormField({ label, type = 'text' }) {
const id = useId();
return (
);
}
// Multiple IDs liés
function PasswordField() {
const id = useId();
return (
${id}-hint}
/>
${id}-hint}>
Doit contenir au moins 8 caractères
);
}
// Liste de champs
function DynamicForm() {
const baseId = useId();
const [fields, setFields] = useState(['']);
return (
{fields.map((field, index) => (
${baseId}-${index}} />
))}
);
}
useSyncExternalStore
import { useSyncExternalStore } from 'react';
// S'abonner à store externe (Redux, MobX, etc.)
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot // Pour SSR
);
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Toujours online côté serveur
}
return isOnline;
}
// Utilisation
function StatusIndicator() {
const isOnline = useOnlineStatus();
return (
{isOnline ? '🟢 Online' : '🔴 Offline'}
);
}
// Store personnalisé
let listeners = [];
let state = { count: 0 };
const store = {
subscribe(callback) {
listeners.push(callback);
return () => {
listeners = listeners.filter(l => l !== callback);
};
},
getSnapshot() {
return state;
},
increment() {
state = { count: state.count + 1 };
listeners.forEach(l => l());
}
};
function Counter() {
const state = useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
return (
{state.count}
);
}
Custom Hooks
Patterns de base
// Hook de fetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Utilisation
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(/api/users/${userId});
if (loading) return Loading...;
if (error) return Error: {error};
return {user.name};
}
Hooks utilitaires
// useToggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle, setValue];
}
// Utilisation
function Modal() {
const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
return (
<>
{isOpen && setIsOpen(false)} />}
>
);
}
// useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Utilisation
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Faire la recherche
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
setSearchTerm(e.target.value)}
/>
);
}
// useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
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];
}
// Utilisation
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
);
}
// useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// Utilisation
function Clock() {
const [time, setTime] = useState(new Date());
useInterval(() => {
setTime(new Date());
}, 1000);
return {time.toLocaleTimeString()};
}
// useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Utilisation
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isDark = useMediaQuery('(prefers-color-scheme: dark)');
return (
{isMobile ? : }
Theme: {isDark ? 'Dark' : 'Light'}
);
}
// useClickOutside
function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// Utilisation
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef();
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
{isOpen && }
);
}
// useAsync
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback((...params) => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction(...params)
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
}
// Utilisation
function UserProfile({ userId }) {
const { value: user, status, error } = useAsync(
() => fetch(/api/users/${userId}).then(r => r.json()),
true
);
if (status === 'pending') return Loading...;
if (status === 'error') return Error: {error.message};
if (status === 'success') return {user.name};
return null;
}
Patterns avancés
Render Props
// Pattern render prop
function DataFetcher({ url, render }) {
const { data, loading, error } = useFetch(url);
if (loading) return Loading...;
if (error) return Error: {error};
return render(data);
}
// Utilisation
function App() {
return (
(
{users.map(user => (
- {user.name}
))}
)}
/>
);
}
// Avec children function
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return children(position);
}
// Utilisation
function App() {
return (
{({ x, y }) => (
Mouse position: {x}, {y}
)}
);
}
Higher-Order Components (HOC)
// HOC basique
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return Loading...;
return ;
};
}
// Utilisation
const UserListWithLoading = withLoading(UserList);
function App() {
const { data, loading } = useFetch('/api/users');
return ;
}
// HOC avec logique
function withAuth(Component) {
return function WithAuthComponent(props) {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) {
return ;
}
return ;
};
}
// Utilisation
const ProtectedDashboard = withAuth(Dashboard);
// HOC avec configuration
function withTracking(eventName) {
return function (Component) {
return function WithTrackingComponent(props) {
useEffect(() => {
trackEvent(eventName);
}, []);
return ;
};
};
}
const TrackedButton = withTracking('buttonclick')(Button);
Compound Components
// Pattern compound component
const TabsContext = createContext();
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
{children}
);
}
function TabList({ children }) {
return {children};
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
);
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return {children};
}
// Assembler les sous-composants
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Utilisation
function App() {
return (
Tab 1
Tab 2
Tab 3
Content 1
Content 2
Content 3
);
}
State Machines
// Machine à états simple
function useStateMachine(initialState, transitions) {
const [state, setState] = useState(initialState);
const transition = useCallback((action) => {
setState(currentState => {
const nextState = transitions[currentState]?.[action];
return nextState || currentState;
});
}, [transitions]);
return [state, transition];
}
// Utilisation pour formulaire
function FormStateMachine() {
const [state, transition] = useStateMachine('idle', {
idle: {
SUBMIT: 'submitting'
},
submitting: {
SUCCESS: 'success',
ERROR: 'error'
},
success: {
RESET: 'idle'
},
error: {
RETRY: 'submitting',
RESET: 'idle'
}
});
const handleSubmit = async (e) => {
e.preventDefault();
transition('SUBMIT');
try {
await submitForm();
transition('SUCCESS');
} catch (error) {
transition('ERROR');
}
};
return (
{state === 'idle' && (
)}
{state === 'submitting' && Submitting...}
{state === 'success' && (
Success!
)}
{state === 'error' && (
Error occurred
)}
);
}
Performance et optimisation
React.memo
import { memo } from 'react';
// Composant mémorisé (évite re-render si props identiques)
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
console.log('Rendering expensive component');
return (
{/ Rendu complexe /}
);
});
// Avec comparaison personnalisée
const UserCard = memo(
function UserCard({ user }) {
return {user.name};
},
(prevProps, nextProps) => {
// Return true si props égales (skip render)
return prevProps.user.id === nextProps.user.id;
}
);
// Liste optimisée
const ListItem = memo(({ item, onDelete }) => {
console.log('Rendering item:', item.id);
return (
{item.name}
);
});
function List({ items }) {
const handleDelete = useCallback((id) => {
// Delete logic
}, []);
return (
{items.map(item => (
))}
);
}
Code splitting
import { lazy, Suspense } from 'react';
// Lazy loading de composants
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
Loading... Something went wrong.
;
}
return this.props.children;
}
}
function App() {
return (
Virtualisation de listes
// Avec react-window
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
{items[index].name}
);
return (
{Row}
);
}
// Variable size list
import { VariableSizeList } from 'react-window';
function DynamicList({ items }) {
const getItemSize = (index) => {
// Retourner hauteur variable
return items[index].height || 50;
};
const Row = ({ index, style }) => (
{items[index].content}
);
return (
{Row}
);
}
Gestion d’état globale
Context API avancé
// Store avec reducer et context
const StoreContext = createContext();
const DispatchContext = createContext();
const initialState = {
user: null,
theme: 'light',
notifications: []
};
function storeReducer(state, action) {
switch (action.type) {
case 'SETUSER':
return { ...state, user: action.payload };
case 'TOGGLETHEME':
return {
...state,
theme: state.theme === 'light' ? 'dark' : 'light'
};
case 'ADDNOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
default:
return state;
}
}
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(storeReducer, initialState);
return (
{children}
);
}
export function useStore() {
const context = useContext(StoreContext);
if (context === undefined) {
throw new Error('useStore must be used within StoreProvider');
}
return context;
}
export function useDispatch() {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useDispatch must be used within StoreProvider');
}
return context;
}
// Actions helpers
export const actions = {
setUser: (user) => ({ type: 'SETUSER', payload: user }),
toggleTheme: () => ({ type: 'TOGGLETHEME' }),
addNotification: (notification) => ({
type: 'ADDNOTIFICATION',
payload: notification
})
};
// Utilisation
function UserProfile() {
const { user, theme } = useStore();
const dispatch = useDispatch();
const handleThemeToggle = () => {
dispatch(actions.toggleTheme());
};
return (
{user?.name}
);
}
Zustand (alternative légère)
import create from 'zustand';
// Store simple
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// Utilisation
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
{count}
);
}
// Store avec async
const useUserStore = create((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const response = await fetch(/api/users/${id});
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => set({ user: null })
}));
// Avec middleware (persist)
import { persist } from 'zustand/middleware';
const useAuthStore = create(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
clearToken: () => set({ token: null })
}),
{
name: 'auth-storage'
}
)
);
Tests
Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// Test basique
test('renders button with text', () => {
render();
const button = screen.getByText('Click me');
expect(button).toBeInTheDocument();
});
// Test avec interaction
test('increments counter on click', () => {
render( );
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText(/count: 0/i);
fireEvent.click(button);
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
// Test avec user-event (plus réaliste)
test('fills and submits form', async () => {
const handleSubmit = jest.fn();
render();
const user = userEvent.setup();
await user.type(screen.getByLabelText(/name/i), 'John Doe');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com'
});
});
// Test async
test('loads and displays user data', async () => {
render( );
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/john doe/i)).toBeInTheDocument();
});
});
// Test avec mock
test('fetches user data', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'John Doe' })
})
);
render( );
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledWith('/api/users/123');
});
// Test custom hook
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Bonnes pratiques
Structure de composant
// 1. Imports
import { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
// 2. Types/Interfaces (si TypeScript)
interface Props {
title: string;
onSubmit: (data: FormData) => void;
}
// 3. Constantes
const MAXLENGTH = 100;
// 4. Styled components ou CSS modules
const styles = {
container: 'bg-white p-4 rounded',
button: 'px-4 py-2 bg-blue-500 text-white'
};
// 5. Composant
function MyComponent({ title, onSubmit }: Props) {
// 5.1 Hooks d'état
const [value, setValue] = useState('');
const [loading, setLoading] = useState(false);
// 5.2 Hooks de contexte
const { user } = useAuth();
// 5.3 Refs
const inputRef = useRef(null);
// 5.4 Valeurs dérivées
const isValid = value.length > 0 && value.length <= MAX LENGTH;
// 5.5 Callbacks
const handleChange = useCallback((e: ChangeEvent) => {
setValue(e.target.value);
}, []);
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
await onSubmit({ value });
setLoading(false);
}, [value, onSubmit]);
// 5.6 Effects
useEffect(() => {
inputRef.current?.focus();
}, []);
// 5.7 Render
return (
);
}
// 6. PropTypes (si pas TypeScript)
MyComponent.propTypes = {
title: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired
};
// 7. Default props
MyComponent.defaultProps = {
title: 'Default Title'
};
// 8. Export
export default MyComponent;
Conventions de nommage
// Composants: PascalCase
function UserProfile() {}
function ProductCard() {}
// Hooks: camelCase avec préfixe "use"
function useAuth() {}
function useFetch() {}
function useLocalStorage() {}
// Handlers: handle + Event
const handleClick = () => {};
const handleChange = () => {};
const handleSubmit = () => {};
// Callbacks passés en props: on + Event
// Booléens: is/has/should
const isLoading = true;
const hasError = false;
const shouldRender = true;
// Arrays: pluriel
const users = [];
const products = [];
// Event handlers dans composants: handle + ComponentName + Event
function Button() {
const handleButtonClick = () => {};
}
Performance tips
- Utilisez
React.memopour composants purs - Utilisez
useMemopour calculs coûteux - Utilisez
useCallbackpour fonctions passées en props - Lazy load routes et composants lourds
- Virtualisez grandes listes
- Évitez créations d’objets/arrays dans render
- Utilisez keys stables dans listes
- Préférez plusieurs contextes à un gros
- Code split par route
- Utilisez React DevTools Profiler
—
Version: React 18.2+ | Ressources: react.dev, beta.reactjs.org