27 min de lecture · 5 938 mots

React 18+ : Hooks, Context, et Patterns modernes

React 18+ : Hooks, Context, et Patterns modernes
Article · 2025.12.19

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 <h1>Bonjour</h1>;
}

// Avec props
function Welcome({ name }) {
  return <h1>Bonjour {name}</h1>;
}

// Arrow function
const Welcome = ({ name }) => {
  return <h1>Bonjour {name}</h1>;
};

// Return implicite
const Welcome = ({ name }) => <h1>Bonjour {name}</h1>;

// Avec destructuration
const UserCard = ({ user: { name, email, age } }) => (
  <div>
    <h2>{name}</h2>
    <p>{email}</p>
    <span>{age} ans</span>
  </div>
);

// Props par défaut
function Button({ text = 'Cliquer', type = 'button' }) {
  return <button type={type}>{text}</button>;
}

// Rest props
function Input({ label, ...inputProps }) {
  return (
    <label>
      {label}
      <input {...inputProps} />
    </label>
  );
}

TypeScript avec React

// Props interface
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({
  text,
  onClick,
  variant = 'primary',
  disabled = false
}) => (
  <button
    onClick={onClick}
    className={variant}
    disabled={disabled}
  >
    {text}
  </button>
);

// Avec children
interface CardProps {
  title: string;
  children: React.ReactNode;
}

const Card: React.FC<CardProps> = ({ title, children }) => (
  <div className="card">
    <h2>{title}</h2>
    {children}
  </div>
);

// Props avec génériques
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Event handlers
interface FormProps {
  onSubmit: (data: FormData) => void;
}

const Form: React.FC<FormProps> = ({ onSubmit }) => {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // ...
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ...
  };

  return <form onSubmit={handleSubmit}>...</form>;
};

Hooks essentiels

useState

import { useState } from 'react';

// État simple
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// 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 <button onClick={increment}>{count}</button>;
}

// État objet
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const handleChange = (e) => {
    setUser({
      ...user,
      [e.target.name]: e.target.value
    });
  };

  return (
    <form>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
      />
      <input
        name="email"
        value={user.email}
        onChange={handleChange}
      />
    </form>
  );
}

// É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 (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <span
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
            onClick={() => toggleTodo(todo.id)}
          >
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>X</button>
        </div>
      ))}
    </div>
  );
}

// Lazy initial state (pour calculs coûteux)
function ExpensiveComponent() {
  const [data, setData] = useState(() => {
    const initialData = computeExpensiveValue();
    return initialData;
  });

  return <div>{data}</div>;
}

// État avec TypeScript
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

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 <div>Chargement...</div>;
  if (error) return <div>Erreur: {error}</div>;
  return <div>{user?.name}</div>;
}

// 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 <div>{size.width} x {size.height}</div>;
}

// 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 (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 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 (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header className={theme}>
      <button onClick={toggleTheme}>
        Toggle Theme
      </button>
    </header>
  );
}

// Context avec TypeScript
interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(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 (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

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 (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <Router>
            <Routes />
          </Router>
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

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 (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

// Auto-focus au mount
function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

// 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 (
    <div>
      <p>{count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

// 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 (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// 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) => (
  <input ref={ref} className="fancy" {...props} />
));

function Parent() {
  const inputRef = useRef();

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>
        Focus Input
      </button>
    </div>
  );
}

// TypeScript avec useRef
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(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 (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

// Reducer complexe avec payload
const initialState = {
  todos: [],
  filter: 'all'
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, done: false }
        ]
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, done: !todo.done }
            : todo
        )
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };

    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const addTodo = (text) => {
    dispatch({ type: 'ADD_TODO', payload: text });
  };

  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  const deleteTodo = (id) => {
    dispatch({ type: 'DELETE_TODO', payload: id });
  };

  return (
    <div>
      {/* UI */}
    </div>
  );
}

// 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 'set_error':
      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 (
    <ul>
      {sortedItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// 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 (
    <div>
      {processedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// É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 <div>{data?.name}</div>;
}

// Contexte value mémorisé
function MyProvider({ children }) {
  const [state, setState] = useState(initialState);

  const value = useMemo(() => ({
    state,
    setState,
    // autres fonctions...
  }), [state]);

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}

// 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 (
    <div>
      <p>Count: {count}</p>
      <Child onIncrement={increment} />
      <Form onSubmit={handleSubmit} />
    </div>
  );
}

// Composant enfant mémorisé (ne re-render que si props changent)
const Child = memo(({ onIncrement }) => {
  console.log('Child rendered');
  return <button onClick={onIncrement}>+</button>;
});

// 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 (
    <div>
      <SearchInput onSearch={search} />
    </div>
  );
}

// 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 (
    <form onSubmit={handleSubmit}>
      <input name="name" onChange={handleChange} />
      <input name="email" onChange={handleChange} />
    </form>
  );
}

// TypeScript
const handleClick = useCallback((id: string) => {
  console.log('Clicked:', id);
}, []);

const handleChange = useCallback(
  (e: React.ChangeEvent<HTMLInputElement>) => {
    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 (
    <>
      <button ref={buttonRef}>{children}</button>
      <div
        ref={tooltipRef}
        style={{
          position: 'fixed',
          left: coords.x,
          top: coords.y
        }}
      >
        {text}
      </div>
    </>
  );
}

// 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 <div ref={divRef} style={{ transform: 'translateX(-100px)' }} />;
}

// 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 (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <div>Recherche...</div>}
      <ResultsList results={results} />
    </div>
  );
}

// Tabs avec transition
function Tabs() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const selectTab = (nextTab) => {
    startTransition(() => {
      setTab(nextTab);
    });
  };

  return (
    <div>
      <button onClick={() => selectTab('home')}>Home</button>
      <button onClick={() => selectTab('profile')}>Profile</button>
      <button onClick={() => selectTab('settings')}>Settings</button>

      {isPending && <Spinner />}

      {tab === 'home' && <HomeContent />}
      {tab === 'profile' && <ProfileContent />}
      {tab === 'settings' && <SettingsContent />}
    </div>
  );
}

useDeferredValue

import { useDeferredValue, useState, memo } from 'react';

// Différer valeur pour optimiser performance
function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <SearchResults query={deferredQuery} />
    </div>
  );
}

// SearchResults ne re-render pas à chaque frappe
const SearchResults = memo(({ query }) => {
  const results = useMemo(() => {
    return searchDatabase(query);  // Opération coûteuse
  }, [query]);

  return (
    <ul>
      {results.map(result => (
        <li key={result.id}>{result.name}</li>
      ))}
    </ul>
  );
});

// Avec indication de chargement
function ProductFilter() {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input onChange={(e) => setFilter(e.target.value)} />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <ProductList filter={deferredFilter} />
      </div>
    </div>
  );
}

useId

import { useId } from 'react';

// ID unique pour accessibilité
function FormField({ label, type = 'text' }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Multiple IDs liés
function PasswordField() {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>Password</label>
      <input
        id={id}
        type="password"
        aria-describedby={`${id}-hint`}
      />
      <div id={`${id}-hint`}>
        Doit contenir au moins 8 caractères
      </div>
    </div>
  );
}

// Liste de champs
function DynamicForm() {
  const baseId = useId();
  const [fields, setFields] = useState(['']);

  return (
    <div>
      {fields.map((field, index) => (
        <div key={index}>
          <label htmlFor={`${baseId}-${index}`}>
            Champ {index + 1}
          </label>
          <input id={`${baseId}-${index}`} />
        </div>
      ))}
    </div>
  );
}

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 (
    <div>
      {isOnline ? '🟢 Online' : '🔴 Offline'}
    </div>
  );
}

// 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 (
    <div>
      <p>{state.count}</p>
      <button onClick={() => store.increment()}>+</button>
    </div>
  );
}

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 <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user.name}</div>;
}

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 (
    <>
      <button onClick={toggleOpen}>Toggle</button>
      {isOpen && <ModalContent onClose={() => 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 (
    <input
      value={searchTerm}
      onChange={(e) => 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 (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}

// 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 <div>{time.toLocaleTimeString()}</div>;
}

// 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 (
    <div>
      {isMobile ? <MobileView /> : <DesktopView />}
      Theme: {isDark ? 'Dark' : 'Light'}
    </div>
  );
}

// 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 (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && <DropdownMenu />}
    </div>
  );
}

// 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 <div>Loading...</div>;
  if (status === 'error') return <div>Error: {error.message}</div>;
  if (status === 'success') return <div>{user.name}</div>;
  return null;
}

Patterns avancés

Render Props

// Pattern render prop
function DataFetcher({ url, render }) {
  const { data, loading, error } = useFetch(url);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return render(data);
}

// Utilisation
function App() {
  return (
    <DataFetcher
      url="/api/users"
      render={(users) => (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    />
  );
}

// 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 (
    <MouseTracker>
      {({ x, y }) => (
        <div>Mouse position: {x}, {y}</div>
      )}
    </MouseTracker>
  );
}

Higher-Order Components (HOC)

// HOC basique
function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// Utilisation
const UserListWithLoading = withLoading(UserList);

function App() {
  const { data, loading } = useFetch('/api/users');
  return <UserListWithLoading isLoading={loading} users={data} />;
}

// HOC avec logique
function withAuth(Component) {
  return function WithAuthComponent(props) {
    const { isAuthenticated, user } = useAuth();

    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }

    return <Component {...props} user={user} />;
  };
}

// Utilisation
const ProtectedDashboard = withAuth(Dashboard);

// HOC avec configuration
function withTracking(eventName) {
  return function (Component) {
    return function WithTrackingComponent(props) {
      useEffect(() => {
        trackEvent(eventName);
      }, []);

      return <Component {...props} />;
    };
  };
}

const TrackedButton = withTracking('button_click')(Button);

Compound Components

// Pattern compound component
const TabsContext = createContext();

function Tabs({ children, defaultValue }) {
  const [activeTab, setActiveTab] = useState(defaultValue);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);

  return (
    <button
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext);

  if (activeTab !== value) return null;
  return <div className="tab-panel">{children}</div>;
}

// Assembler les sous-composants
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Utilisation
function App() {
  return (
    <Tabs defaultValue="tab1">
      <Tabs.List>
        <Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
        <Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
        <Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="tab1">
        <h2>Content 1</h2>
      </Tabs.Panel>
      <Tabs.Panel value="tab2">
        <h2>Content 2</h2>
      </Tabs.Panel>
      <Tabs.Panel value="tab3">
        <h2>Content 3</h2>
      </Tabs.Panel>
    </Tabs>
  );
}

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 (
    <div>
      {state === 'idle' && (
        <form onSubmit={handleSubmit}>
          <button type="submit">Submit</button>
        </form>
      )}

      {state === 'submitting' && <div>Submitting...</div>}

      {state === 'success' && (
        <div>
          Success!
          <button onClick={() => transition('RESET')}>Reset</button>
        </div>
      )}

      {state === 'error' && (
        <div>
          Error occurred
          <button onClick={() => transition('RETRY')}>Retry</button>
          <button onClick={() => transition('RESET')}>Reset</button>
        </div>
      )}
    </div>
  );
}

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 (
    <div>
      {/* Rendu complexe */}
    </div>
  );
});

// Avec comparaison personnalisée
const UserCard = memo(
  function UserCard({ user }) {
    return <div>{user.name}</div>;
  },
  (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 (
    <div>
      {item.name}
      <button onClick={() => onDelete(item.id)}>Delete</button>
    </div>
  );
});

function List({ items }) {
  const handleDelete = useCallback((id) => {
    // Delete logic
  }, []);

  return (
    <div>
      {items.map(item => (
        <ListItem
          key={item.id}
          item={item}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

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 (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// 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 <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Loading />}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// Preload sur hover
function Navigation() {
  const [DashboardModule, setDashboardModule] = useState(null);

  const preloadDashboard = () => {
    import('./Dashboard').then(module => {
      setDashboardModule(module);
    });
  };

  return (
    <nav>
      <Link
        to="/dashboard"
        onMouseEnter={preloadDashboard}
      >
        Dashboard
      </Link>
    </nav>
  );
}

Virtualisation de listes

// Avec react-window
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// 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 }) => (
    <div style={style}>
      {items[index].content}
    </div>
  );

  return (
    <VariableSizeList
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableSizeList>
  );
}

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 'SET_USER':
      return { ...state, user: action.payload };

    case 'TOGGLE_THEME':
      return {
        ...state,
        theme: state.theme === 'light' ? 'dark' : 'light'
      };

    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload]
      };

    default:
      return state;
  }
}

export function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, initialState);

  return (
    <StoreContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StoreContext.Provider>
  );
}

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: 'SET_USER', payload: user }),
  toggleTheme: () => ({ type: 'TOGGLE_THEME' }),
  addNotification: (notification) => ({
    type: 'ADD_NOTIFICATION',
    payload: notification
  })
};

// Utilisation
function UserProfile() {
  const { user, theme } = useStore();
  const dispatch = useDispatch();

  const handleThemeToggle = () => {
    dispatch(actions.toggleTheme());
  };

  return (
    <div className={theme}>
      <h1>{user?.name}</h1>
      <button onClick={handleThemeToggle}>Toggle Theme</button>
    </div>
  );
}

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 (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

// 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(<Button>Click me</Button>);
  const button = screen.getByText('Click me');
  expect(button).toBeInTheDocument();
});

// Test avec interaction
test('increments counter on click', () => {
  render(<Counter />);

  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(<Form onSubmit={handleSubmit} />);

  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(<UserProfile userId="123" />);

  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(<UserProfile userId="123" />);

  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 MAX_LENGTH = 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<HTMLInputElement>(null);

  // 5.4 Valeurs dérivées
  const isValid = value.length > 0 && value.length <= MAX_LENGTH;

  // 5.5 Callbacks
  const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    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 (
    <form onSubmit={handleSubmit} className={styles.container}>
      <h2>{title}</h2>
      <input
        ref={inputRef}
        value={value}
        onChange={handleChange}
        maxLength={MAX_LENGTH}
      />
      <button
        type="submit"
        disabled={!isValid || loading}
        className={styles.button}
      >
        {loading ? 'Loading...' : 'Submit'}
      </button>
    </form>
  );
}

// 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
<Button onClick={handleClick} />
<Form onSubmit={handleSubmit} />

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

  1. Utilisez React.memo pour composants purs
  2. Utilisez useMemo pour calculs coûteux
  3. Utilisez useCallback pour fonctions passées en props
  4. Lazy load routes et composants lourds
  5. Virtualisez grandes listes
  6. Évitez créations d’objets/arrays dans render
  7. Utilisez keys stables dans listes
  8. Préférez plusieurs contextes à un gros
  9. Code split par route
  10. 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, choisis celui qui te convient.

Laisser un commentaire