🚀 PREPARETOI Premium — Accédez à tous les examens et certifications illimitées
React Intermédiaire

React Avancé : Maîtriser les Patterns Professionnels et l'Architecture Scalable

Plongez dans les patterns React modernes et les architectures éprouvées en production pour construire des applications robustes et maintenables. Découvrez comment les équipes seniors structurent leur code et gèrent la complexité à grande échelle.

Preparetoi.academy 120 min 7 vues

1. Les Hooks Personnalisés : Réutilisabilité et Abstraction

Définition

Les hooks personnalisés (custom hooks) sont des fonctions JavaScript qui encapsulent la logique React réutilisable en extrayant l'état et les effets secondaires dans des fonctions indépendantes. Ils permettent de partager la logique entre composants sans modifier la structure du composant.

Explication

Dans les applications professionnelles, on observe rapidement que certaines logiques métier se répètent : gestion de requêtes API, validation de formulaires, gestion d'authentification, pagination. Les hooks personnalisés offrent une solution élégante pour extraire cette logique réutilisable. Contrairement aux HOC (Higher Order Components) ou aux render props, les custom hooks offrent une syntaxe plus simple et une meilleure lisibilité. Ils suivent la convention de nommage "useXxx" et peuvent utiliser tous les hooks React intégrés.

Les custom hooks sont particulièrement puissants car ils permettent de :

  • Partager la logique stateful sans imbrication de composants
  • Encapsuler des comportements complexes dans une interface simple
  • Tester la logique métier indépendamment des composants
  • Créer des abstractions qui masquent la complexité technique

Bloc de code

// Hook personnalisé pour la gestion des requêtes API
const useAPI = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [refetch, setRefetch] = useState(0);

  useEffect(() => {
    let isMounted = true;
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, {
          headers: { 'Content-Type': 'application/json', ...options.headers },
          ...options,
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const result = await response.json();
        if (isMounted) setData(result);
      } catch (err) {
        if (isMounted) setError(err.message);
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchData();
    return () => { isMounted = false; };
  }, [url, refetch]);

  return { data, loading, error, refetch: () => setRefetch(prev => prev + 1) };
};

// Utilisation dans un composant
function UsersList() {
  const { data: users, loading, error, refetch } = useAPI('/api/users');

  if (loading) return <div>Chargement...</div>;
  if (error) return <div>Erreur: {error}</div>;

  return (
    <div>
      <button onClick={refetch}>Rafraîchir</button>
      {users?.map(user => <div key={user.id}>{user.name}</div>)}
    </div>
  );
}

// Hook pour la validation de formulaire
const useForm = (initialValues, onSubmit) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

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

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await onSubmit(values);
    } finally {
      setIsSubmitting(false);
    }
  };

  return { values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting };
};

Tableau comparatif

Aspect Custom Hooks HOC Render Props
Syntaxe Simple et intuitive Imbrication de composants Fonction callback
Composition Linéaire Imbriquée Imbriquée
Debugging Clair dans React DevTools Wrapper components visibles Callback visibles
Performance Pas de composant supplémentaire HOC crée un composant wrapper Pas de wrapper
Réutilisabilité Très élevée Moyenne Moyenne
Courbe apprentissage Faible Moyenne Moyenne-Haute

Astuce Pro

Nommez vos custom hooks avec le préfixe "use" pour que les règles ESLint des hooks fonctionnent correctement. Créez une dépendance explicite dans useEffect pour éviter les bugs subtils de fermetures (closure bugs).

⚠️ Attention

N'appelez pas les hooks conditionnellement ou dans des boucles - cela viole les "Rules of Hooks" et causera des bugs imprévisibles. Toujours appeler les hooks au niveau racine du composant ou d'un autre hook.


2. Context API et State Management Avancé

Définition

La Context API est un mécanisme React qui permet de passer des données à travers l'arbre des composants sans passer les props manuellement à chaque niveau. Combinée avec des patterns avancés, elle constitue une solution complète pour le state management en applications intermédiaires à complexes.

Explication

La Context API résout le problème du "prop drilling" - passer des props à travers plusieurs niveaux de composants même s'ils n'en ont pas besoin directement. Dans les applications réelles, gérer le thème, l'authentification, les préférences utilisateur ou les données globales devient rapidement fastidieux avec les props simples.

La solution professionnelle combine Context + useReducer pour créer un state management complet et maintenable. Ce pattern :

  • Centralise la logique métier dans un reducer
  • Permet des transitions d'état prévisibles
  • Facilite le debugging avec Redux DevTools
  • Offre une API type Redux mais sans dépendance externe
  • Scalable pour les applications intermédiaires (au-delà, Redux/Zustand est recommandé)

Bloc de code

// Création d'un contexte avec reducer (pattern complet)
import { createContext, useContext, useReducer, useCallback } from 'react';

// Types d'actions
const ACTIONS = {
  LOGIN: 'LOGIN',
  LOGOUT: 'LOGOUT',
  LOAD_USER: 'LOAD_USER',
  UPDATE_PROFILE: 'UPDATE_PROFILE',
  SET_ERROR: 'SET_ERROR',
};

// État initial
const initialState = {
  user: null,
  isAuthenticated: false,
  loading: false,
  error: null,
};

// Reducer
const authReducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.LOAD_USER:
      return { ...state, loading: true, error: null };
    case ACTIONS.LOGIN:
      return {
        ...state,
        user: action.payload,
        isAuthenticated: true,
        loading: false,
        error: null,
      };
    case ACTIONS.LOGOUT:
      return { ...state, user: null, isAuthenticated: false };
    case ACTIONS.UPDATE_PROFILE:
      return {
        ...state,
        user: { ...state.user, ...action.payload },
      };
    case ACTIONS.SET_ERROR:
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
};

// Contexte
const AuthContext = createContext();

// Provider
export const AuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, initialState);

  const login = useCallback(async (credentials) => {
    dispatch({ type: ACTIONS.LOAD_USER });
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
      });
      const user = await response.json();
      dispatch({ type: ACTIONS.LOGIN, payload: user });
      return user;
    } catch (error) {
      dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
      throw error;
    }
  }, []);

  const logout = useCallback(() => {
    dispatch({ type: ACTIONS.LOGOUT });
  }, []);

  const updateProfile = useCallback((profileData) => {
    dispatch({ type: ACTIONS.UPDATE_PROFILE, payload: profileData });
  }, []);

  const value = {
    ...state,
    login,
    logout,
    updateProfile,
  };

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

// Hook personnalisé pour utiliser le contexte
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth doit être utilisé à l\'intérieur d\'AuthProvider');
  }
  return context;
};

// Utilisation
function LoginForm() {
  const { login, loading, error } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login({ email, password });
      // Redirection effectuée automatiquement
    } catch (err) {
      console.error('Login failed:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button disabled={loading}>{loading ? 'Connexion...' : 'Se connecter'}</button>
      {error && <div className="error">{error}</div>}
    </form>
  );
}

// Optimisation avec useMemo pour éviter les re-renders inutiles
export const OptimizedAuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, initialState);

  const value = useMemo(() => ({
    ...state,
    login: useCallback(async (credentials) => { /* ... */ }, []),
    logout: useCallback(() => { /* ... */ }, []),
    updateProfile: useCallback((data) => { /* ... */ }, []),
  }), [state]);

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

Tableau comparatif

Critère Context + useReducer Redux Zustand Jotai
Bundle size Intégré (0kb) ~9kb ~2kb ~3kb
DevTools Manuel Excellent Bon Bon
Scalabilité Moyenne Excellente Très bonne Bonne
Courbe apprentissage Basse Moyenne-Haute Basse Basse
Middleware Non Oui (puissant) Limité Oui
Performance Correcte (beware renders) Optimisée Optimisée Optimisée
Idéal pour Petits-moyens apps Grands projets Tous tailles Léger

Astuce Pro

Divisez vos contextes par domaine (Auth, Theme, Notifications, UI) plutôt qu'un megacontext. Utilisez useMemo pour mémoriser la valeur du contexte afin d'éviter les re-renders innecessaires de tous les consommateurs.

⚠️ Attention

Tous les consommateurs d'un contexte se re-rendent quand sa valeur change, même si seule une partie de l'état affecte ce composant. Pour des applications complexes avec état fréquemment changeant, préférez Redux ou Zustand. Testez toujours les performances avec React DevTools Profiler.


3. Optimisation des Performances : Mémorisation et Code Splitting

Définition

L'optimisation des performances en React implique de réduire les re-rendus inutiles (via useMemo, useCallback, memo), diviser le code en chunks chargés à la demande (code splitting), et implémenter le lazy loading des composants pour améliorer le temps de chargement initial et la réactivité de l'application.

Explication

Les applications React peuvent devenir lentes à cause de re-rendus excessifs et de bundles JavaScript volumineux. En production, une application de 5MB prend 5-10 secondes à charger sur une connexion 4G. L'optimisation des performances n'est pas un luxe mais une nécessité.

Les trois axes d'optimisation principaux sont :

  1. Réduction des re-rendus : utiliser React.memo, useMemo, useCallback pour mémoriser composants et valeurs
  2. Code splitting : diviser le bundle en chunks chargés à la demande avec React.lazy et Suspense
  3. Bundle analysis : identifier et éliminer les dépendances inutiles

Dans les équipes professionnelles, ces optimisations suivent une métrique stricte : Core Web Vitals (LCP, FID, CLS) et Lighthouse scores.

Bloc de code

// 1. React.memo pour mémoriser un composant
const UserCard = React.memo(({ user, onAction }) => {
  console.log('UserCard renderé:', user.id);
  return (
    <div>
      <h3>{user.name}</h3>
      <button onClick={() => onAction(user.id)}>Action</button>
    </div>
  );
}, (prevProps, nextProps) => {
  // Retourner true si les props sont égales (ne pas re-render)
  return prevProps.user.id === nextProps.user.id &&
         prevProps.onAction === nextProps.onAction;
});

// 2. useCallback pour mémoriser les fonctions
function UsersList() {
  const [users, setUsers] = useState([]);
  const [selectedId, setSelectedId] = useState(null);

  // Sans useCallback : onAction est recréée à chaque render
  // Tous les UserCard se re-rendent même si props inchangé
  const onAction = useCallback((userId) => {
    setSelectedId(userId);
    console.log('Action pour utilisateur:', userId);
  }, []); // Dépendances vides = fonction jamais recréée

  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} onAction={onAction} />
      ))}
    </div>
  );
}

// 3. useMemo pour les calculs coûteux
function DataAnalytics({ rawData }) {
  const processedData = useMemo(() => {
    console.log('Calcul coûteux effectué');
    return rawData
      .filter(item => item.valid)
      .map(item => ({ ...item, score: calculateScore(item) }))
      .sort((a, b) => b.score - a.score);
  }, [rawData]); // Recalculé seulement si rawData change

  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>{item.name}: {item.score}</div>
      ))}
    </div>
  );
}

// 4. Code splitting avec React.lazy et Suspense
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Router>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/reports" element={<Reports />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

// 5. Route-based code splitting (pattern professionnel)
const routeModules = {
  '/dashboard': lazy(() => import('./pages/Dashboard')),
  '/profile': lazy(() => import('./pages/Profile')),
  '/admin': lazy(() => import('./pages/Admin')),
  '/checkout': lazy(() => import('./pages/Checkout')),
};

function Router({ currentRoute }) {
  const Component = routeModules[currentRoute];
  return (
    <Suspense fallback={<div>Chargement...</div>}>
      {Component && <Component />}
    </Suspense>
  );
}

// 6. Optimisation avec useDeferredValue (React 18)
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const results = useMemo(
    () => expensiveSearch(deferredQuery),
    [deferredQuery]
  );

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

// 7. Pattern avancé : composant virtualisé pour grandes listes
import { FixedSizeList } from 'react-window';

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

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

Tableau comparatif

Technique Cas d'usage Impact Complexité DevTools
React.memo Composants purs, props stables Moyen Basse Clair
useCallback Callbacks passés en props Moyen Basse Clair
useMemo Calculs coûteux répétés Élevé Basse-Moyenne Visible
Code splitting Routes, modales lourdes Très élevé Moyenne Clair
Virtualisation Listes 10k+ items Très élevé Moyenne-Haute Externe
Image optimization Images JPG/PNG Élevé Basse DevTools

Astuce Pro

Mesurez avant d'optimiser ! Utilisez React DevTools Profiler et Lighthouse. 80% des gains viennent souvent de 20% des optimisations. Priorité : code splitting des routes > virtualisation des listes > React.memo sélectif.

⚠️ Attention

Trop de useMemo et useCallback peuvent ralentir l'application car leur comparaison a un coût. N'optimisez que les paths critiques détectés par le Profiler. Mémoriser chaque fonction est contre-productif. Redux DevTools aide à identifier les re-rendus excessifs.


4. Gestion des Effets Secondaires avec useEffect : Patterns Avancés

Définition

useEffect est le hook qui gère les opérations secondaires (side effects) : appels API, souscriptions, timers, manipulation du DOM. Maîtriser les dépendances, le nettoyage et les patterns avancés est crucial pour éviter les bugs subtils et les fuites mémoire.

Explication

useEffect exécute du code après le rendu. C'est où la plupart des bugs React se cachent : re-exécutions excessives, nettoyage oublié, conditions de race. Les équipes professionnelles suivent des patterns stricts pour éviter ces pièges.

Les points clés :

  • Dépendances : liste précise des valeurs qui déclenchent l'effet
  • Nettoyage : fonction de cleanup pour éviter les fuites mémoire
  • Ordre d'exécution : effect après rendu, cleanup avant prochain effect ou unmount
  • Patterns courants : initialisation, polling, debouncing, conditions de course

Bloc de code

// 1. Pattern basique avec dépendances correctes
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let isMounted = true; // Flag pour éviter setState après unmount

    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        if (isMounted) { // Vérifier avant setState
          setUser(data);
        }
      } catch (error) {
        if (isMounted) console.error(error);
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchUser();

    // Cleanup : annuler les operations en cours à l'unmount
    return () => {
      isMounted = false;
    };
  }, [userId]); // Re-exécuter SEULEMENT si userId change

  return loading ? <div>Chargement...</div> : <div>{user?.name}</div>;
}

// 2. Pattern avec AbortController (meilleure solution)
function UserProfileOptimized({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();

    fetch(`/api/users/${userId}`, { signal: abortController.signal })
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => abortController.abort(); // Annuler la requête en cours
  }, [userId]);

  return <div>{user?.name}</div>;
}

// 3. Pattern avec debounce pour recherche
function SearchUsers({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    const timerId = setTimeout(async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();
        setResults(data);
      } finally {
        setLoading(false);
      }
    }, 300); // Attendre 300ms après la dernière keystroke

    return () => clearTimeout(timerId); // Cleanup du timeout
  }, [query]);

  return (
    <div>
      {results.map(r => <div key={r.id}>{r.name}</div>)}
    </div>
  );
}

// 4. Pattern avec souscription (WebSocket, EventEmitter)
function NotificationCenter() {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/notifications');

    ws.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      setNotifications(prev => [notification, ...prev]);
    };

    ws.onerror = (error) => console.error('WebSocket error:', error);

    // Cleanup critique : fermer la connexion
    return () => {
      ws.close();
    };
  }, []); // Exécuté une seule fois au mount

  return (
    <div>
      {notifications.map((n, i) => (
        <div key={i}>{n.message}</div>
      ))}
    </div>
  );
}

// 5. Pattern avec synchronisation multiple
function SyncedEditor({ docId }) {
  const [content, setContent] = useState('');
  const [saved, setSaved] = useState(true);

  // Effet 1 : charger le document au mount
  useEffect(() => {
    fetch(`/api/docs/${docId}`)
      .then(r => r.json())
      .then(doc => setContent(doc.content));
  }, [docId]);

  // Effet 2 : sauvegarder après 1 seconde d'inactivité
  useEffect(() => {
    setSaved(false);
    const timer = setTimeout(async () => {
      await fetch(`/api/docs/${docId}`, {
        method: 'PATCH',
        body: JSON.stringify({ content }),
      });
      setSaved(true);
    }, 1000);

    return () => clearTimeout(timer);
  }, [content, docId]);

  // Effet 3 : avertir avant de quitter si non sauvegardé
  useEffect(() => {
    const handleBeforeUnload = (e) => {
      if (!saved) {
        e.preventDefault();
        e.returnValue = 'Changements non sauvegardés';
      }
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [saved]);

  return (
    <div>
      <textarea value={content} onChange={(e) => setContent(e.target.value)} />
      <span>{saved ? '✓ Sauvegardé' : '⏳ Sauvegarde...'}</span>
    </div>
  );
}

// 6. Custom hook pour lifecycle avancé
const useAsync = (asyncFunction, immediate = true) => {
  const [status, setStatus] = useState('idle');
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!immediate) return;

    let isMounted = true;
    const execute = async () => {
      setStatus('pending');
      try {
        const response = await asyncFunction();
        if (isMounted) {
          setResult(response);
          setStatus('success');
        }
      } catch (error) {
        if (isMounted) {
          setError(error);
          setStatus('error');
        }
      }
    };

    execute();
    return () => { isMounted = false; };
  }, [asyncFunction, immediate]);

  return { status, result, error };
};

// Utilisation du custom hook
function DataComponent() {
  const { status, result, error } = useAsync(
    () => fetch('/api/data').then(r => r.json())
  );

  if (status === 'pending') return <div>Chargement...</div>;
  if (status === 'error') return <div>Erreur: {error.message}</div>;
  return <div>{JSON.stringify(result)}</div>;
}

Tableau comparatif

Pattern Cas d'usage Cleanup nécessaire Complexité Risques
Fetch simple Données initiales Non si AbortController Basse Race condition
Polling Données temps réel Oui (clearInterval) Basse Fuites mémoire
Debounce Recherche, validation Oui (clearTimeout) Moyenne Timing bugs
WebSocket Live updates Oui (close()) Moyenne Pas de nettoyage
Souscription Observer pattern Oui (unsubscribe) Moyenne Mémory leaks
Multiple effects Complex flows Oui (chacun) Haute Spaghetti code

Astuce Pro

Utilisez AbortController plutôt que isMounted - c'est la solution moderne et recommandée. Divisez les effects en plusieurs useEffect pour chaque responsabilité plutôt qu'un mega effect avec 10 dépendances.

⚠️ Attention

Oublier une dépendance dans le tableau causes des bugs subtils difficiles à détecter. Utiliser le plugin ESLint "react-hooks" est obligatoire. Les race conditions (requête A puis B, mais B finit avant A) surviennent facilement - testez avec des délais réseau simulés.


5. Patterns de Composition et Architecture Scalable

Définition

Les patterns de composition en React structurent comment les composants collaborent et partagent la responsabilité. Une architecture scalable organise le code en couches (presentation, logique métier, données) pour maintenir la maintenabilité à mesure que l'application grandit.

Explication

Les applications qui commencent simples deviennent complexes rapidement. Sans architecture, on aboutit à des composants "foireux" (god components) avec 500 lignes, imbrications profondes et logique métier dispersée. Les équipes seniors utilisent des patterns éprouvés.

Les patterns principaux :

  • Container/Presentational : séparer logique (container) et présentation (presentational)
  • Compound Components : composants collaborant ensemble (Select/Option, Tabs/TabPanel)
  • Render Props avancées : partager logique complexe
  • Feature Folder Architecture : organiser par fonctionnalité plutôt que par type
  • Atomic Design : hiérarchie atoms -> molecules -> organisms

Bloc de code

// 1. Container/Presentational Pattern
// Presentational : pure, pas de logique métier
const UserCardPresentation = ({ user, onEdit, onDelete, isLoading }) => (
  <div className="user-card">
    <h3>{user.name}</h3>
    <p>{user.email}</p>
    <button onClick={onEdit} disabled={isLoading}>Éditer</button>
    <button onClick={onDelete} disabled={isLoading}>Supprimer</button>
  </div>
);

// Container : logique métier, state management
const UserCardContainer = ({ userId }) => {
  const { user, updateUser, deleteUser, loading } = useAPI(`/api/users/${userId}`);
  const navigate = useNavigate();

  const handleEdit = async (newData) => {
    await updateUser(newData);
    // logique métier
  };

  const handleDelete = async () => {
    await deleteUser();
    navigate('/users');
  };

  return (
    <UserCardPresentation
      user={user}
      onEdit={handleEdit}
      onDelete={handleDelete}
      isLoading={loading}
    />
  );
};

export default UserCardContainer;

// 2. Compound Components Pattern
// Exemple : Select/Option
const SelectContext = createContext();

const Select = ({ value, onChange, children }) => {
  return (
    <SelectContext.Provider value={{ value, onChange }}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
};

const SelectOption = ({ value, label }) => {
  const { value: selectedValue, onChange } = useContext(SelectContext);
  return (
    <button
      className={selectedValue === value ? 'selected' : ''}
      onClick={() => onChange(value)}
    >
      {label
🚀 Support IT Moderne

Maîtrisez le support informatique moderne : Cloud, cybersécurité, IA et automatisation avec un guide complet et orienté pratique.

Découvrir le livre →
Accédez à des centaines d'examens QCM — Découvrir les offres Premium