
Il y a une poignée de hooks React que presque personne n'utilise. Ils sont dans la doc, ils sont livrés avec React, mais ils n'apparaissent jamais dans les tutos ou les code reviews.
useId. useDebugValue. useSyncExternalStore. useDeferredValue. useTransition.
Tu as probablement scrollé devant. Peut-être que tu as lu la description et pensé "j'apprendrai ça quand j'en aurai besoin."
Voilà le truc : tu en as probablement besoin maintenant. Tu ne le sais juste pas encore.
useId : le hook d'accessibilité
Tu construis un formulaire. Tu dois connecter les labels aux inputs :
function EmailField() {
return (
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" />
</div>
);
}
Simple. Jusqu'à ce que tu rendes deux composants EmailField sur la même page. Maintenant tu as deux éléments avec id="email". HTML invalide. Accessibilité cassée.
L'ancien fix :
function EmailField() {
const [id] = useState(() => `email-${Math.random().toString(36).slice(2)}`);
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</div>
);
}
Ça marche... jusqu'au server-side rendering. Le serveur génère un ID, le client en génère un autre. Mismatch d'hydratation. React te crie dessus.
useId à la rescousse
function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</div>
);
}
useId génère un ID unique qui est stable entre serveur et client. Pas de mismatch d'hydratation. Pas d'IDs dupliqués. Ça marche tout simplement.
Plusieurs IDs depuis un seul appel
Tu as besoin de plusieurs IDs liés ? Utilise un seul useId comme préfixe :
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={`${id}-input`}>{label}</label>
<input id={`${id}-input`} aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>Ce champ est requis</p>
</div>
);
}
Un appel de hook, trois éléments connectés. Propre et accessible.
Quand utiliser useId
- Labels de formulaire et inputs
- Attributs ARIA (aria-labelledby, aria-describedby)
- Chaque fois que tu as besoin d'un ID unique dans un composant réutilisable
Quand NE PAS utiliser useId
- Keys dans les listes (utilise les IDs des données)
- Sélecteurs CSS (ne te fie pas aux IDs générés pour le styling)
useDebugValue : rends React DevTools utile
Tu as construit un custom hook. Il marche super bien. Mais quand tu l'inspectes dans React DevTools, tu vois... rien d'utile.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
Dans DevTools, tu vois : OnlineStatus: true
Pas super utile. C'est bien ? C'est mal ?
Ajoute du contexte avec useDebugValue
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useDebugValue(isOnline ? '🟢 En ligne' : '🔴 Hors ligne');
// ... reste du hook
return isOnline;
}
Maintenant DevTools affiche : OnlineStatus: "🟢 En ligne"
Instantanément clair.
Diffère le formatage coûteux
Si ta valeur de debug nécessite un calcul coûteux, diffère-le :
function useDataFetch(url) {
const [data, setData] = useState(null);
// Ne calcule la string de debug que quand DevTools est ouvert
useDebugValue(data, (data) => {
if (!data) return 'Chargement...';
return `${data.items.length} éléments chargés à ${new Date().toLocaleTimeString()}`;
});
// ... logique de fetch
return data;
}
Le deuxième argument est une fonction de formatage. Elle ne s'exécute que quand tu inspectes vraiment le composant dans DevTools.
Quand utiliser useDebugValue
- Custom hooks dans des librairies partagées
- Hooks complexes où le state interne n'est pas évident
- Debug pendant le développement
Quand NE PAS utiliser useDebugValue
- Chaque custom hook (overkill)
- Builds de production (c'est un no-op en prod de toute façon)
- Hooks simples où la valeur de retour est évidente
useSyncExternalStore : connecte-toi au monde extérieur
React gère son propre state. Mais parfois tu dois te synchroniser avec des données externes :
- APIs navigateur (statut en ligne, taille de fenêtre, media queries)
- Librairies de state tierces (Redux, Zustand sans leurs bindings React)
- Connexions WebSocket
- LocalStorage
L'approche naïve :
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
Ça marche pour les cas simples. Mais ça a des problèmes avec le rendu concurrent. React pourrait "déchirer" ton UI : certains composants voient l'ancienne valeur, d'autres la nouvelle.
useSyncExternalStore fait ça correctement
function useWindowWidth() {
return useSyncExternalStore(
// subscribe: comment écouter les changements
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
// getSnapshot: comment obtenir la valeur actuelle
() => window.innerWidth
);
}
useSyncExternalStore garantit la consistance. Tous les composants voient la même valeur pendant un render.
Exemple concret : statut en ligne
function useOnlineStatus() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine,
() => true // Snapshot serveur : on assume en ligne
);
}
Le troisième argument est pour le SSR. Comme il n'y a pas de navigator sur le serveur, on fournit un fallback.
Exemple concret : media queries
function useMediaQuery(query) {
return useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false // Fallback serveur
);
}
// Utilisation
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileNav /> : <DesktopNav />;
}
Quand utiliser useSyncExternalStore
- APIs navigateur qui changent dans le temps
- Gestion de state externe sans bindings React
- Toute source de données basée sur des subscriptions
Quand NE PAS utiliser useSyncExternalStore
- State React (utilise useState/useReducer)
- Fetching de données (utilise useEffect ou une librairie comme TanStack Query)
- Lectures uniques (pas besoin de subscription)
useDeferredValue : garde l'UI réactive
Tu as un input de recherche qui filtre une énorme liste :
function Search() {
const [query, setQuery] = useState('');
const filteredItems = filterItems(items, query); // Coûteux !
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<List items={filteredItems} />
</>
);
}
Chaque frappe déclenche le filtrage. Avec 10 000 items, l'input devient lent.
Diffère la partie coûteuse
function Search() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Le filtrage utilise la valeur différée
const filteredItems = useMemo(
() => filterItems(items, deferredQuery),
[deferredQuery]
);
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<List items={filteredItems} />
</>
);
}
Maintenant l'input se met à jour immédiatement (avec query), tandis que la liste se met à jour avec un léger délai (avec deferredQuery). L'UI reste réactive.
Affiche le contenu périmé pendant la mise à jour
Tu peux détecter quand la valeur différée est périmée :
function Search() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filteredItems = useMemo(
() => filterItems(items, deferredQuery),
[deferredQuery]
);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<List items={filteredItems} />
</div>
</>
);
}
La liste devient semi-transparente pendant la mise à jour, donnant un feedback visuel.
Quand utiliser useDeferredValue
- Renders coûteux basés sur l'input utilisateur
- Quand tu ne contrôles pas la source de la valeur
- Afficher du contenu périmé est acceptable
useTransition : prends le contrôle des mises à jour
useDeferredValue diffère une valeur. useTransition diffère une mise à jour.
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab); // Cette mise à jour est "basse priorité"
});
}
return (
<>
<TabButtons onSelect={selectTab} isPending={isPending} />
<TabContent tab={tab} />
</>
);
}
Quand tu cliques sur un onglet, React le marque comme une "transition." Si l'utilisateur clique sur un autre onglet avant que le premier rende, React abandonne le premier render et démarre le second.
La différence avec useDeferredValue
| useDeferredValue | useTransition |
|---|---|
| Diffère une valeur | Diffère une mise à jour de state |
| Tu ne contrôles pas la source | Tu contrôles la mise à jour |
| Retourne la valeur différée | Retourne [isPending, startTransition] |
Exemple concret : navigation
function Router() {
const [page, setPage] = useState('/home');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
return (
<>
<Nav onNavigate={navigate} />
{isPending && <LoadingBar />}
<Page url={page} />
</>
);
}
La nav reste interactive pendant que la page charge. La barre de chargement montre la progression.
Quand utiliser useTransition
- Changement d'onglets
- Navigation
- Toute mise à jour où interactivité > render immédiat
La cheat sheet
| Hook | But | Utilise quand |
|---|---|---|
useId | Génère des IDs uniques et stables | Accessibilité formulaires, attributs ARIA |
useDebugValue | Labellise les hooks dans DevTools | Debug de custom hooks |
useSyncExternalStore | S'abonne aux données externes | APIs navigateur, state externe |
useDeferredValue | Diffère une valeur | Tu ne contrôles pas la source |
useTransition | Diffère une mise à jour | Tu contrôles la mise à jour du state |
La suite ?
Tu connais maintenant tous les hooks livrés avec React 18.
Mais React 19 change la donne. Nouveaux hooks pour les formulaires, les mises à jour optimistes, et le state async. Server components et actions. Un tout nouveau modèle mental.
Dans le dernier article, on explorera les nouveaux hooks de React 19 et ce qu'ils signifient pour le futur.
Continuer vers la partie 5 : La nouvelle ère React 19 →
Des questions ? Trouvé un bug dans mon code ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.