Custom hooks · HOC · Compound components · Headless
Réutiliser de la logique · Composer des interfaces · Typer avec TypeScript
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
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
Custom hooks
Extraire et réutiliser de la logique entre composants
Un custom hook est JUSTE une fonction JavaScript
qui commence par use et qui appelle d'autres hooks
Ce que c'est
Ce que ce n'est pas
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
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')
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')
Règles des hooks
Pourquoi le préfixe use est obligatoire
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.
✗ 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.
Higher-Order Components (HOC)
Wrapper pattern pour la composition
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} />
}
}
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
Custom Hook
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).
Compound Components
API déclarative avec Context
Comme Radix UI, Headless UI, ou shadcn/ui
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>
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
}
Un composant headless fournit la logique et l'accessibilité
sans imposer de rendu visuel — zéro styles
Exemples célèbres
Avantages
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} />} />
Construire un Accordion
Avec le pattern compound components + Context
// 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.
// 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>
}
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>
}
// 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è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
✗ 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])
}
✗ 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()
// ...
}
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