Patterns avancés React

Custom hooks · HOC · Compound components · Headless

Réutiliser de la logique · Composer des interfaces · Typer avec TypeScript

Objectifs de la leçon

1. Custom hooks

Extraire et réutiliser de la logique entre composants

2. Règles des hooks

Pourquoi le préfixe use et les règles de React

3. Higher-Order Components

Wrapper pattern pour la composition

4. Compound components

API déclarative avec Context (Radix, Headless UI)

5. Composants génériques TypeScript

Typer les props dynamiquement avec les generics

Plan du cours

1. Custom hooks

Extraire la logique dans une fonction réutilisable

2. Règles des hooks

Le préfixe use et pourquoi il est obligatoire

3. Higher-Order Components (HOC)

Pattern pré-hooks encore présent dans les codebases

4. Compound components

Partager l'état avec Context entre composants enfants

5. Live coding : Accordion

Construire un Accordion complet avec le pattern compound

Module 1

Custom hooks

Extraire et réutiliser de la logique entre composants

Qu'est-ce qu'un custom hook ?

Un custom hook est JUSTE une fonction JavaScript

qui commence par use et qui appelle d'autres hooks

Ce que c'est

  • Une fonction qui appelle useState, useEffect
  • Commence par use
  • Peut retourner n'importe quoi
  • Réutilisable dans plusieurs composants

Ce que ce n'est pas

  • Pas de magie React cachée
  • Pas un nouveau concept de React
  • Pas une classe ou un composant
  • Pas un hook si elle n'appelle aucun hook

Problème : logique dupliquée dans 2 composants

On gère la même logique de fetch dans deux endroits différents

// UserProfile.tsx

function UserProfile() {

const [data, setData] = useState(null)

const [loading, setLoading] = useState(true)

useEffect(() => {

fetch('/api/user')

.then(r => r.json())

.then(d => { setData(d); setLoading(false) })

}, [])

// ← logique dupliquée !

}

// ProductList.tsx

function ProductList() {

const [data, setData] = useState(null)

const [loading, setLoading] = useState(true)

useEffect(() => {

fetch('/api/products')

.then(r => r.json())

.then(d => { setData(d); setLoading(false) })

}, [])

// ← copié-collé !

}

La règle : dès qu'une logique apparaît dans 2+ composants → extraire dans un custom hook

Solution : le custom hook useFetch

On extrait la logique dans une fonction réutilisable

// hooks/useFetch.ts

function useFetch(url: string) {

const [data, setData] = useState<unknown>(null)

const [loading, setLoading] = useState(true)

const [error, setError] = useState<string | null>(null)

useEffect(() => {

fetch(url)

.then(r => r.json())

.then(d => { setData(d); setLoading(false) })

.catch(e => setError(e.message))

}, [url])

return { data, loading, error }

}

// UserProfile.tsx

const { data, loading } = useFetch('/api/user')

// ProductList.tsx

const { data, loading } = useFetch('/api/products')

Autre exemple : useLocalStorage

Synchroniser un état React avec le localStorage du navigateur

function useLocalStorage<T>(key: string, initialValue: T) {

const [value, setValue] = useState<T>(() => {

const stored = localStorage.getItem(key)

return stored ? JSON.parse(stored) : initialValue

})

const setStoredValue = (val: T) => {

setValue(val)

localStorage.setItem(key, JSON.stringify(val))

}

return [value, setStoredValue] as const

}

// Dans un composant

const [theme, setTheme] = useLocalStorage('theme', 'dark')

Module 2

Règles des hooks

Pourquoi le préfixe use est obligatoire

Les 2 règles des hooks React

Règle 1 — Appeler les hooks au niveau supérieur

N'appelez jamais un hook dans une condition, une boucle ou une fonction imbriquée. React doit toujours appeler les hooks dans le même ordre à chaque rendu.

Règle 2 — Appeler les hooks uniquement dans des fonctions React

Appelez les hooks depuis des composants fonctionnels React ou depuis des custom hooks. Jamais depuis une fonction JavaScript ordinaire.

Pourquoi le préfixe use ?

React utilise ce préfixe comme convention pour détecter automatiquement les violations des règles (via l'eslint-plugin-react-hooks et les DevTools). Sans ce préfixe, React ne peut pas vous avertir si vous cassez les règles.

Règle 1 en pratique : ordre constant

✗ ERREUR — hook conditionnel

function MyComponent({ isAdmin }) {

// React perd l'ordre si isAdmin change !

if (isAdmin) {

const user = useUser()

}

const [count, setCount] = useState(0)

}

✓ CORRECT — hooks inconditionnels

function MyComponent({ isAdmin }) {

// Toujours appelé, dans le même ordre

const user = useUser()

const [count, setCount] = useState(0)

// La condition porte sur le résultat

if (!isAdmin) return null

}

React stocke les valeurs des hooks dans un tableau interne, indexé par ordre d'appel. Si l'ordre change entre deux rendus, les valeurs sont mélangées → bugs silencieux.

Module 3

Higher-Order Components (HOC)

Wrapper pattern pour la composition

Qu'est-ce qu'un HOC ?

Un HOC est une fonction qui prend un composant en entrée

et retourne un nouveau composant avec des comportements ajoutés

// Signature d'un HOC

const withSomething = <P extends object>(

WrappedComponent: React.ComponentType<P>

) => {

return function EnhancedComponent(props: P) {

// Ajouter du comportement ici

return <WrappedComponent {...props} />

}

}

Exemple : withAuth

Protéger un composant qui nécessite une authentification

function withAuth<P extends object>(Component: React.ComponentType<P>) {

return function WithAuth(props: P) {

const { isAuthenticated } = useAuth()

if (!isAuthenticated) {

return <Navigate to="/login" />

}

return <Component {...props} />

}

}

// Utilisation

const ProtectedDashboard = withAuth(Dashboard)

const ProtectedProfile = withAuth(Profile)

HOC vs Custom Hook : lequel choisir ?

HOC

  • Modifie le rendu (ajoute du JSX)
  • Pattern pré-hooks (avant 2019)
  • Présent dans les codebases anciens
  • Peut empiler plusieurs HOC
  • Difficile à déboguer (wrapper hell)
  • Risque de collision de props

Custom Hook

  • Partage de la logique uniquement
  • Approche moderne recommandée
  • Plus lisible et testable
  • Compose facilement
  • Pas de composant supplémentaire
  • Visible dans les DevTools

Règle pratique : si un hook suffit → utilisez un hook. Un HOC s'impose seulement si vous devez envelopper le rendu (ex. : error boundary, router guards).

Module 4

Compound Components

API déclarative avec Context

Comme Radix UI, Headless UI, ou shadcn/ui

Le problème : prop drilling

Sans compound components, on passe tout en props → rigide et verbeux

✗ API rigide avec props

<Tabs

items={[

{ label: "Profile", content: <Profile/> },

{ label: "Settings", content: <Settings/> },

]}

activeTab={activeTab}

onTabChange={setActiveTab}

/>

Impossible de personnaliser le rendu des onglets

✓ API déclarative compound

<Tabs>

<Tabs.List>

<Tabs.Trigger value="profile">

🧑 Profile

</Tabs.Trigger>

</Tabs.List>

<Tabs.Content value="profile">

<Profile/>

</Tabs.Content>

</Tabs>

Comment ça fonctionne : Context interne

Le composant parent crée un Context que les enfants consomment

1️⃣ Créer le Context

const AccordionContext = createContext<AccordionContextType | null>(null)

2️⃣ Le parent fournit l'état (React 19 : plus besoin de .Provider)

function Accordion({ children }) {

const [openItem, setOpenItem] = useState<string | null>(null)

return <AccordionContext value={{ openItem, setOpenItem }}>

{children}

</AccordionContext>

}

3️⃣ Les enfants consomment le Context

function AccordionItem({ value, children }) {

const { openItem, setOpenItem } = useContext(AccordionContext)

const isOpen = openItem === value

}

Composants headless

Un composant headless fournit la logique et l'accessibilité

sans imposer de rendu visuel — zéro styles

Exemples célèbres

  • Radix UI — primitives headless
  • Headless UI — Tailwind Labs
  • shadcn/ui — Radix + Tailwind
  • TanStack Table

Avantages

  • Accessibilité (ARIA) incluse
  • Style 100% libre
  • Compatible avec n'importe quel design system
  • Logique keyboard/focus gérée

Composants génériques TypeScript

Typer les props dynamiquement avec les generics

// Un composant List générique — fonctionne avec n'importe quel type

interface ListProps<T> {

items: T[]

renderItem: (item: T, index: number) => React.ReactNode

keyExtractor: (item: T) => string

}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {

return <ul>{items.map((item, i) => (

<li key={keyExtractor(item)}>{renderItem(item, i)}</li>

))}</ul>

}

// TypeScript infère T = User automatiquement

<List items={users} keyExtractor={u => u.id} renderItem={u => <UserCard user={u} />} />

Module 5 — Live coding

Construire un Accordion

Avec le pattern compound components + Context

Étape 1 — Context et types

// accordion/Accordion.tsx

import { createContext, useContext, useState } from 'react'

interface AccordionContextType {

openItem: string | null

toggle: (value: string) => void

}

const AccordionContext = createContext<AccordionContextType | null>(null)

function useAccordion() {

const ctx = useContext(AccordionContext)

if (!ctx) throw new Error('useAccordion must be used within Accordion')

return ctx

}

Le hook interne useAccordion protège contre l'utilisation hors contexte avec un message d'erreur clair.

Étape 2 — Composant racine et Item

// Le composant racine

function Accordion({ children }: { children: React.ReactNode }) {

const [openItem, setOpenItem] = useState<string | null>(null)

const toggle = (value: string) =>

setOpenItem(prev => prev === value ? null : value)

return (

<AccordionContext value={{ openItem, toggle }}>

<div className="divide-y divide-slate-700">{children}</div>

</AccordionContext>

)

}

// AccordionItem : reçoit value + children

function AccordionItem({ value, children }: { value: string; children: React.ReactNode }) {

return <div>{children}</div>

}

Étape 3 — Trigger et Content

function AccordionTrigger({ value, children }: { value: string; children: React.ReactNode }) {

const { openItem, toggle } = useAccordion()

const isOpen = openItem === value

return (

<button

onClick={() => toggle(value)}

aria-expanded={isOpen}

className="w-full flex justify-between p-4 text-left"

>

{children}

<span>{isOpen ? '▲' : '▼'}</span>

</button>

)

}

function AccordionContent({ value, children }: { value: string; children: React.ReactNode }) {

const { openItem } = useAccordion()

if (openItem !== value) return null

return <div className="p-4">{children}</div>

}

Étape 4 — Assembler et exporter

// Attacher les sous-composants au parent

Accordion.Item = AccordionItem

Accordion.Trigger = AccordionTrigger

Accordion.Content = AccordionContent

export default Accordion

// Utilisation finale — API déclarative, lisible

<Accordion>

<Accordion.Item value="q1">

<Accordion.Trigger value="q1">C'est quoi React ?</Accordion.Trigger>

<Accordion.Content value="q1">Une librairie UI de Meta.</Accordion.Content>

</Accordion.Item>

<Accordion.Item value="q2">

<Accordion.Trigger value="q2">Qu'est-ce qu'un hook ?</Accordion.Trigger>

<Accordion.Content value="q2">Une fonction qui commence par use.</Accordion.Content>

</Accordion.Item>

</Accordion>

Pièges courants — vue d'ensemble

⚠ Piège 1

Hook sans hook interne

Une fonction qui n'appelle aucun hook n'est pas un hook — c'est une fonction utilitaire. Ne la nommez pas use…

⚠ Piège 2

Oublier le préfixe use

Sans use, React ne peut pas appliquer les règles des hooks → violations silencieuses, bugs difficiles à tracer

⚠ Piège 3

HOC quand un hook suffirait

Les HOC sont un pattern pré-hooks. Si vous partagez de la logique (pas du JSX), un custom hook est plus simple et lisible

⚠ Piège 4

Trop imbriquer les compound components

Gardez 2-3 niveaux max. Au-delà, la lisibilité disparaît et le Context devient difficile à tracer

Piège 1 — Hook sans hook interne

✗ ERREUR — mauvais nommage

// Ça s'appelle useFormatDate mais

// n'appelle aucun hook React !

function useFormatDate(date: Date) {

return date.toLocaleDateString('fr-FR')

}

// Ce n'est PAS un hook !

// C'est une fonction utilitaire

✓ CORRECT — nommer correctement

// Fonction utilitaire sans use

function formatDate(date: Date) {

return date.toLocaleDateString('fr-FR')

}

// Un vrai hook utilise des hooks

function useFormattedDate(date: Date) {

return useMemo(() => formatDate(date), [date])

}

Piège 3 — HOC superflu

✗ ERREUR — HOC pour de la logique

function withWindowSize(Comp) {

return function(props) {

const [size, setSize] = useState(...)

useEffect(...)

return <Comp {...props} size={size} />

}

}

// Wrapper inutile, props polluées

✓ CORRECT — un hook suffit

function useWindowSize() {

const [size, setSize] = useState(...)

useEffect(...)

return size

}

function MyComponent() {

const size = useWindowSize()

// ...

}

Points clés à retenir

Custom hook

JUSTE une fonction qui appelle des hooks — extraire dès que la logique est utilisée dans 2+ composants

Préfixe use

Obligatoire — sans lui, React ne peut pas appliquer les règles des hooks ni vous avertir des violations

HOC

Pattern pré-hooks encore présent dans les codebases — préférer un hook quand il n'y a pas de JSX à envelopper

Compound components

Context interne pour partager l'état entre enfants — API déclarative lisible, 2-3 niveaux max

Composants génériques TypeScript

Les generics permettent de typer les props dynamiquement — TypeScript infère le type automatiquement à l'usage