
Tu maîtrises les bases. useState, useEffect, useContext. Tu sais quand utiliser useRef, useMemo, useCallback.
Mais il y a un troisième tier de hooks que la plupart des devs ne touchent jamais. Ils regardent useReducer et pensent "overkill." Ils voient useLayoutEffect et pensent "c'est pas juste useEffect ?" Ils lisent useImperativeHandle et ferment l'onglet.
Voilà le truc : ces hooks ne sont pas compliqués. Ils résolvent des problèmes spécifiques que les hooks de base ne peuvent pas résoudre. Et une fois que tu vois ces problèmes, tu ne peux plus les ignorer.
Débloquons de nouveaux patterns.
useReducer : quand useState devient le bordel
Tu commences avec un formulaire simple :
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
Puis les requirements grandissent :
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [touched, setTouched] = useState({});
const [isValid, setIsValid] = useState(false);
Sept appels useState. Sept setters. Des mises à jour de state éparpillées dans les handlers. Bonne chance pour tracker ce qui déclenche quoi.
useReducer entre en jeu
const initialState = {
name: '',
email: '',
age: 0,
isSubmitting: false,
error: null,
touched: {},
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
touched: { ...state.touched, [action.field]: true }
};
case 'SUBMIT_START':
return { ...state, isSubmitting: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false };
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, error: action.error };
case 'RESET':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Form() {
const [state, dispatch] = useReducer(formReducer, initialState);
function handleChange(e) {
dispatch({
type: 'SET_FIELD',
field: e.target.name,
value: e.target.value
});
}
async function handleSubmit(e) {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (err) {
dispatch({ type: 'SUBMIT_ERROR', error: err.message });
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={state.name} onChange={handleChange} />
<input name="email" value={state.email} onChange={handleChange} />
<button disabled={state.isSubmitting}>
{state.isSubmitting ? 'Envoi...' : 'Envoyer'}
</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
Pourquoi c'est mieux
-
Toutes les transitions de state au même endroit. Tu veux savoir ce qui se passe quand le submit échoue ? Regarde
SUBMIT_ERROR. C'est tout. -
Les états impossibles deviennent impossibles. Avec des useState séparés, tu pourrais accidentellement avoir
isSubmitting: trueETerror: "quelque chose". Avec un reducer, chaque action définit exactement à quoi ressemble le prochain state. -
Facile à tester. Un reducer est une fonction pure. State en entrée + action = state en sortie. Pas besoin de React.
test('SUBMIT_ERROR définit l erreur et arrête le submitting', () => {
const state = { isSubmitting: true, error: null };
const action = { type: 'SUBMIT_ERROR', error: 'Réseau échoué' };
const result = formReducer(state, action);
expect(result.isSubmitting).toBe(false);
expect(result.error).toBe('Réseau échoué');
});
Le modèle mental
Pense à useReducer comme une machine à états.
Chaque action est une transition. Chaque état est un nœud. Tu peux visualiser tout le comportement de ton app.
Quand utiliser useReducer vs useState
| Utilise useState quand... | Utilise useReducer quand... |
|---|---|
| Le state est une valeur simple | Le state est un objet avec plusieurs champs |
| Les updates sont indépendantes | Les updates dépendent les unes des autres |
| La logique est simple | La logique a plusieurs branches |
| Tu as 1-3 variables de state | Tu as 4+ variables de state liées |
useLayoutEffect : quand useEffect arrive trop tard
Quiz rapide : qu'est-ce qui ne va pas avec ce tooltip ?
function Tooltip({ text, targetRect }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef(null);
useEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setPosition({
x: targetRect.left,
y: targetRect.top - height
});
}, [targetRect]);
return (
<div ref={ref} style={{ position: 'absolute', left: position.x, top: position.y }}>
{text}
</div>
);
}
Réponse : il clignote.
Voici la timeline :
- Le composant rend à la position (0, 0)
- Le navigateur peint le tooltip à (0, 0)
- useEffect s'exécute, mesure la hauteur, met à jour la position
- Le composant re-rend à la bonne position
- Le navigateur peint à nouveau
L'utilisateur voit le tooltip sauter. C'est le clignotement.
useLayoutEffect à la rescousse
function Tooltip({ text, targetRect }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef(null);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setPosition({
x: targetRect.left,
y: targetRect.top - height
});
}, [targetRect]);
return (
<div ref={ref} style={{ position: 'absolute', left: position.x, top: position.y }}>
{text}
</div>
);
}
Même code, hook différent. Plus de clignotement.
La différence
useLayoutEffect s'exécute de façon synchrone après les mutations DOM mais avant que le navigateur peigne. L'utilisateur ne voit jamais l'état intermédiaire.
Quand utiliser useLayoutEffect
- Mesurer des éléments DOM (largeur, hauteur, position)
- Mettre à jour le DOM de façon synchrone basé sur des mesures
- Éviter le clignotement visuel
- Animations qui doivent démarrer immédiatement
Quand NE PAS utiliser useLayoutEffect
- Fetch de données (utilise useEffect)
- Subscriptions (utilise useEffect)
- Tout ce qui n'a pas besoin de mesures DOM
useLayoutEffect bloque le paint du navigateur. Utilise-le seulement quand tu as besoin de travail DOM synchrone.
useImperativeHandle : expose une API custom
Parfois, un composant parent a besoin de faire quelque chose à un enfant. Focus un input. Scroll vers une position. Déclencher une animation.
L'approche naïve :
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
inputRef.current.scrollIntoView();
inputRef.current.style.backgroundColor = 'yellow'; // Aïe
}
return <CustomInput ref={inputRef} />;
}
Le parent a un accès total au nœud DOM. Il peut tout faire. C'est le problème.
Expose seulement ce que tu veux
function CustomInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView({ behavior: 'smooth' });
}
// Pas d'accès au style, value, ou quoi que ce soit d'autre
}), []);
return <input ref={inputRef} className="custom-input" />;
}
Maintenant le parent peut seulement appeler focus() et scrollIntoView(). Rien d'autre.
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // Marche
inputRef.current.scrollIntoView(); // Marche
inputRef.current.style.backgroundColor = 'yellow'; // undefined, ne fait rien
}
return <CustomInput ref={inputRef} />;
}
Un exemple concret : lecteur vidéo
function VideoPlayer({ ref, src }) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
},
pause() {
videoRef.current.pause();
},
seek(time) {
videoRef.current.currentTime = time;
},
getCurrentTime() {
return videoRef.current.currentTime;
}
}), []);
return (
<div className="video-container">
<video ref={videoRef} src={src} />
{/* Contrôles custom, overlays, etc. */}
</div>
);
}
function App() {
const playerRef = useRef(null);
return (
<>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.pause()}>Pause</button>
<button onClick={() => playerRef.current.seek(0)}>Recommencer</button>
</>
);
}
Le parent contrôle la vidéo via une API propre. Il ne peut pas toucher à la structure DOM interne.
Le modèle mental
Pense à useImperativeHandle comme la construction d'une télécommande pour ton composant.
Le parent parle à la télécommande. La télécommande parle aux internals du composant. Le parent ne touche jamais les internals directement.
Quand utiliser useImperativeHandle
- Tu construis une librairie de composants réutilisables
- Tu veux cacher les détails d'implémentation
- Tu as besoin de méthodes impératives mais tu veux contrôler ce qui est exposé
- Tu wraps une structure DOM complexe (lecteurs vidéo, éditeurs rich text, etc.)
La cheat sheet
| Hook | Résout | Utilise quand |
|---|---|---|
useReducer | Logique de state complexe | 4+ valeurs de state liées, patterns machine à états |
useLayoutEffect | Clignotement visuel | Mesures DOM, updates synchrones avant le paint |
useImperativeHandle | Accès impératif contrôlé | Exposer des APIs propres, cacher les détails d'implémentation |
Tout assembler
Voici un composant qui utilise les trois :
function Modal({ ref, children }) {
const [state, dispatch] = useReducer(modalReducer, {
isOpen: false,
position: { x: 0, y: 0 }
});
const contentRef = useRef(null);
// Expose une API propre au parent
useImperativeHandle(ref, () => ({
open() { dispatch({ type: 'OPEN' }); },
close() { dispatch({ type: 'CLOSE' }); }
}), []);
// Mesure et positionne avant le paint
useLayoutEffect(() => {
if (state.isOpen && contentRef.current) {
const { width, height } = contentRef.current.getBoundingClientRect();
dispatch({
type: 'SET_POSITION',
position: {
x: (window.innerWidth - width) / 2,
y: (window.innerHeight - height) / 2
}
});
}
}, [state.isOpen]);
if (!state.isOpen) return null;
return (
<div className="modal-overlay">
<div
ref={contentRef}
className="modal-content"
style={{ left: state.position.x, top: state.position.y }}
>
{children}
</div>
</div>
);
}
- useReducer gère le state open/close et la position ensemble
- useLayoutEffect centre la modal sans clignotement
- useImperativeHandle donne au parent une simple API
open()/close()
La suite ?
Tu connais maintenant les hooks qui couvrent 99% du développement React.
Mais React continue d'évoluer. React 19 apporte une nouvelle génération de hooks pour les formulaires, les updates optimistes, et le state async. Ils changent complètement notre façon de penser les interactions serveur.
Dans le prochain article, on explorera les hooks React 19 et ce qu'ils signifient pour le futur de React.
Continuer vers la partie 4 : Les hooks que tu as oubliés →
Des questions ? Trouvé un bug dans mon code ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.