19 min de lecture · 4 047 mots

React 18+ : Hooks, Context, et Patterns modernes

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: 'set
error'; 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...
}> } /> } /> } /> ); } // Avec error boundary class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error('Error:', error, errorInfo); } render() { if (this.state.hasError) { return
Something went wrong.
; } return this.props.children; } } function App() { return ( }> ); } // Preload sur hover function Navigation() { const [DashboardModule, setDashboardModule] = useState(null); const preloadDashboard = () => { import('./Dashboard').then(module => { setDashboardModule(module); }); }; 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 <= MAXLENGTH;

  // 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 (
    

{title}

); } // 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.memo pour composants purs
  • Utilisez useMemo pour calculs coûteux
  • Utilisez useCallback pour 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

Une remarque, un retour ?

Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.