React 19 useEffectEvent et Activity : nouvelles API et questions d'entretien 2026
Analyse approfondie des API useEffectEvent et Activity de React 19.2 avec exemples de code, correction des fermetures obsoletes, pre-rendu en arriere-plan et questions d'entretien technique.

React 19.2 a introduit deux APIs qui transforment la gestion des effets de bord et la navigation dans les applications React modernes : useEffectEvent et le composant <Activity>. La premiere resout un probleme que tout developpeur React a rencontre au moins une fois -- les fermetures obsoletes (stale closures) dans les effets. La seconde offre un mecanisme de pre-rendu en arriere-plan avec preservation d'etat, impossible a reproduire proprement avec les outils existants. Ces deux APIs, disponibles depuis octobre 2025, apparaissent deja dans les entretiens techniques React des entreprises qui ont adopte la derniere version du framework.
useEffectEvent et Activity ne sont disponibles qu'a partir de React 19.2. La mise a jour s'effectue via npm install react@latest react-dom@latest. Le plugin ESLint eslint-plugin-react-hooks@6+ reconnait nativement useEffectEvent dans les tableaux de dependances et applique automatiquement les regles specifiques a cette API.
Le piege des fermetures obsoletes dans les effets
Le systeme d'effets de React repose sur un principe simple : lorsqu'une valeur du tableau de dependances change, l'effet se resynchronise. Ce mecanisme fonctionne parfaitement pour les valeurs qui doivent effectivement controler le cycle de vie de l'effet. Le probleme survient lorsqu'un effet a besoin de lire une valeur reactive sans que cette lecture ne doive provoquer une resynchronisation.
Le cas classique est celui d'un hook de connexion a un chat. L'effet doit se reconnecter uniquement quand la salle change (roomId), mais le callback de message doit lire le theme courant pour le tracking analytique. Avant React 19.2, la seule solution propre etait le pattern useRef :
// Before useEffectEvent: useRef workaround
import { useEffect, useRef } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Store theme in a ref to avoid stale closure
const themeRef = useRef(theme)
themeRef.current = theme
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', (msg) => {
// themeRef.current always has the latest value
logAnalytics('new_message', { roomId, theme: themeRef.current })
showNotification(msg)
})
connection.connect()
return () => connection.disconnect()
}, [roomId]) // theme intentionally excluded — but linter warns
}Ce contournement remplit son role, mais il presente trois defauts significatifs. Le linter ESLint signale theme comme dependance manquante, ce qui oblige a supprimer l'avertissement manuellement. Le code masque l'intention architecturale : un developpeur qui decouvre ce hook doit reconstituer mentalement pourquoi theme transite par une ref au lieu de figurer dans le tableau de dependances. Enfin, le pattern useRef ajoute du code boilerplate qui detourne l'attention de la logique metier.
useEffectEvent : separer logique reactive et actions secondaires
Le hook useEffectEvent cree une fonction stable qui accede en permanence aux dernieres valeurs de props et de state, sans jamais apparaitre dans le graphe de dependances de l'effet. La terminologie est deliberee : un Effect Event est un evenement declenche par un effet, et non une partie de sa logique de synchronisation.
// After useEffectEvent: clean separation of concerns
import { useEffect, useEffectEvent } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Effect Event: always reads latest theme, never triggers reconnect
const onMessage = useEffectEvent((msg: string) => {
logAnalytics('new_message', { roomId, theme })
showNotification(msg)
})
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', onMessage)
connection.connect()
return () => connection.disconnect()
}, [roomId]) // No linter warning — onMessage is an Effect Event
}Le gain de clarte est immediat. La fonction onMessage isole la logique de reaction a un message (analytics et notification) de la logique de connexion (ouverture, ecoute, deconnexion). theme est lu directement dans le corps de l'Effect Event sans contournement. Le tableau de dependances ne contient que roomId, ce qui correspond exactement a la semantique souhaitee : la connexion ne se retablit que lors d'un changement de salle. Le linter ne produit aucun avertissement car il reconnait onMessage comme un Effect Event.
Regles d'utilisation et contraintes de useEffectEvent
Trois regles encadrent l'utilisation de useEffectEvent :
- Declaration au niveau racine -- L'appel doit se situer au niveau superieur d'un composant ou d'un hook personnalise, jamais dans une boucle ou une condition.
- Appel exclusivement depuis un effet -- La fonction retournee ne doit etre invoquee que depuis l'interieur d'un
useEffectou d'un autre Effect Event. - Interdiction de transmission -- Un Effect Event ne doit jamais etre passe en prop a un composant enfant ni retourne par un hook pour consommation externe.
La violation de la regle 2 entraine des bugs subtils : l'identite de la fonction change a chaque rendu, et la stocker dans une ref ou la transmettre a un composant enfant annule le benefice du mecanisme. Le plugin eslint-plugin-react-hooks@6+ detecte automatiquement ces violations.
// Correct: Effect Event used inside useEffect
import { useEffect, useEffectEvent, useState } from 'react'
interface SearchTrackerProps {
query: string
userId: string
}
export function SearchTracker({ query, userId }: SearchTrackerProps) {
const [results, setResults] = useState<string[]>([])
// Track searches with current user context
const onSearchComplete = useEffectEvent((resultCount: number) => {
analytics.track('search_complete', {
query,
userId, // Always latest userId
resultCount,
timestamp: Date.now(),
})
})
useEffect(() => {
const controller = new AbortController()
fetchSearchResults(query, controller.signal).then((data) => {
setResults(data)
onSearchComplete(data.length) // Called from inside useEffect
})
return () => controller.abort()
}, [query]) // userId excluded safely — lives in the Effect Event
return <ResultsList results={results} />
}Il serait tentant d'utiliser useEffectEvent pour faire taire tous les avertissements du linter de dependances. Ce serait une erreur. Si une valeur controle reellement le moment ou un effet doit se re-executer, elle appartient au tableau de dependances. useEffectEvent est reserve aux actions secondaires -- analytics, logging, notifications -- qui doivent lire des valeurs reactives sans declencher de resynchronisation. Toute utilisation abusive masque des bugs reels et compromet la fiabilite de l'application.
Le composant Activity : pre-rendu et preservation d'etat
Le composant <Activity> (connu sous le nom de code "Offscreen" pendant sa phase de developpement) introduit un troisieme mode de gestion de la visibilite, situe entre le rendu conditionnel et le masquage CSS.
Le rendu conditionnel ({show && <Component />}) detruit completement le composant et son etat lorsqu'il est masque. Le CSS display: none preserve l'etat et les effets, mais les effets continuent de consommer des ressources (timers, WebSockets, subscriptions). <Activity> preserve l'etat tout en nettoyant les effets et en differant les mises a jour.
// Activity preserves form state across tab switches
import { useState } from 'react'
import { Activity } from 'react'
interface Tab {
id: string
label: string
content: React.ReactNode
}
export function TabLayout({ tabs }: { tabs: Tab[] }) {
const [activeTab, setActiveTab] = useState(tabs[0].id)
return (
<div>
<nav className="flex gap-2 border-b border-border">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'border-b-2 border-primary' : ''}
>
{tab.label}
</button>
))}
</nav>
{tabs.map((tab) => (
<Activity key={tab.id} mode={activeTab === tab.id ? 'visible' : 'hidden'}>
{tab.content}
</Activity>
))}
</div>
)
}Le scenario le plus parlant est celui d'un systeme d'onglets contenant des formulaires. Un utilisateur remplit partiellement un formulaire dans l'onglet "Profil", bascule sur l'onglet "Parametres" pour verifier une information, puis revient sur "Profil". Avec un rendu conditionnel, le formulaire serait reinitialise. Avec <Activity>, l'etat du formulaire -- chaque champ, chaque interaction -- survit au changement d'onglet. Les effets de l'onglet masque sont nettoyes pour liberer les ressources, et ils se remontent automatiquement lorsque l'onglet redevient visible.
Prêt à réussir tes entretiens React / Next.js ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Modes du composant Activity : visible et hidden en detail
Le composant <Activity> accepte une prop mode dont les deux valeurs produisent des comportements radicalement differents au niveau du runtime React :
| Comportement | mode="visible" | mode="hidden" |
|----------|-------------------|------------------|
| DOM rendering | Normal | display: none via CSS |
| Component state | Active | Preserved in memory |
| Effects (useEffect) | Mounted | Cleaned up |
| Update priority | Normal | Deferred to idle |
| Pre-rendering | N/A | Renders at low priority |
La propriete la plus strategique du mode hidden concerne le pre-rendu. Lorsqu'un composant demarre en mode hidden (rendu initial avec mode="hidden"), React le rend a basse priorite sans monter ses effets. Ce mecanisme permet de pre-rendre une page entiere en arriere-plan, de sorte qu'elle soit instantanement disponible lorsque l'utilisateur navigue vers elle.
// Pre-render the next likely route in the background
import { Activity, Suspense, use } from 'react'
interface PrerenderedRouteProps {
isActive: boolean
dataPromise: Promise<DashboardData>
}
export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) {
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent dataPromise={dataPromise} />
</Suspense>
</Activity>
)
}
function DashboardContent({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
// use() reads the cached promise — works during hidden pre-render
const data = use(dataPromise)
return (
<div className="grid grid-cols-3 gap-4">
<MetricsCard data={data.metrics} />
<ChartPanel data={data.charts} />
<RecentActivity items={data.activity} />
</div>
)
}La combinaison d'<Activity> avec Suspense et l'API use constitue le pattern de pre-rendu le plus puissant disponible dans React 19.2. Le <Activity> masque effectue le rendu a priorite basse. Le <Suspense> gere le chargement asynchrone des donnees via la promesse. Lorsque isActive passe a true, le contenu apparait instantanement sans aucun etat de chargement intermediaire.
Activity et TanStack Query : le piege du cache
L'integration de <Activity> avec TanStack Query presente un piege qui merite une attention particuliere. Puisque useQuery repose en interne sur useEffect, et que les effets sont demontes a l'interieur d'un <Activity mode="hidden">, les requetes ne s'executent tout simplement pas pour les composants masques.
// Problem: useQuery won't fetch when Activity is hidden
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'
function UserStats() {
// This useQuery will NOT run while hidden
const { data } = useQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return <StatsDisplay data={data} />
}
// Solution: prefetch data outside the Activity boundary
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
const queryClient = useQueryClient()
// Prefetch at the parent level — runs regardless of Activity mode
queryClient.ensureQueryData({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return (
<Activity mode={showStats ? 'visible' : 'hidden'}>
<UserStats />
</Activity>
)
}La solution consiste a deplacer le prefetch dans le composant parent, en dehors de la frontiere <Activity>. La methode ensureQueryData du QueryClient declenche le fetch uniquement si les donnees ne sont pas deja presentes en cache. Ainsi, lorsque le composant masque redevient visible, les donnees sont deja disponibles et le rendu est instantane.
Le composant Activity echange de la memoire contre de la vitesse de navigation. Chaque arbre de composants masque reste en memoire avec son DOM complet. Pour les applications qui maintiennent de nombreuses routes masquees simultanement, la consommation memoire peut devenir significative. L'equipe React travaille actuellement sur un mecanisme d'eviction automatique des Activity masques les moins recemment utilises, prevu pour une prochaine version.
Combiner useEffectEvent et Activity dans une architecture temps reel
Les deux APIs se completent naturellement dans les architectures orientees temps reel. Le scenario typique est celui d'un dashboard multi-canal avec des connexions WebSocket et du tracking analytique :
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'
function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
const [messages, setMessages] = useState<Message[]>([])
// Analytics tracking with latest userId — no effect re-sync
const onNewMessage = useEffectEvent((msg: Message) => {
analytics.track('live_message', { channel, userId })
setMessages((prev) => [...prev, msg])
})
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/${channel}`)
ws.onmessage = (event) => {
const msg = JSON.parse(event.data) as Message
onNewMessage(msg)
}
return () => ws.close()
}, [channel]) // Clean reconnect only when channel changes
return <MessageList messages={messages} />
}
export function LiveDashboard({ userId }: { userId: string }) {
const [activeChannel, setActiveChannel] = useState('general')
const channels = ['general', 'alerts', 'metrics']
return (
<div>
<nav className="flex gap-2">
{channels.map((ch) => (
<button key={ch} onClick={() => setActiveChannel(ch)}>
{ch}
</button>
))}
</nav>
{channels.map((ch) => (
<Activity key={ch} mode={activeChannel === ch ? 'visible' : 'hidden'}>
<LiveFeed channel={ch} userId={userId} />
</Activity>
))}
</div>
)
}Le fonctionnement de ce composant illustre la synergie entre les deux APIs. useEffectEvent garantit que le tracking analytique reference toujours le userId courant sans provoquer de reconnexion WebSocket. Le composant <Activity> preserve l'historique des messages de chaque canal lorsque l'utilisateur navigue entre les onglets.
Lorsqu'un canal est masque, le WebSocket correspondant est ferme grace au cleanup de l'effet. L'etat local messages est preserve en memoire. Quand l'utilisateur revient sur ce canal, une nouvelle connexion WebSocket est etablie et les messages precedemment recus sont toujours affiches. Ce comportement serait impossible a obtenir avec un rendu conditionnel (perte d'etat) ou CSS display: none (WebSocket maintenu inutilement).
Questions d'entretien technique : useEffectEvent et Activity
Ces questions apparaissent regulierement dans les entretiens techniques React des entreprises ayant migre vers React 19.2. Elles evaluent la comprehension des mecanismes sous-jacents et la capacite a appliquer ces APIs dans des contextes concrets.
Q1 : Quel probleme useEffectEvent resout-il que useCallback ne peut pas resoudre ?
useCallback produit une fonction memoises dont la reference reste stable tant que ses dependances ne changent pas. Cependant, si une de ses dependances evolue, la reference change, ce qui provoque la resynchronisation de tout effet qui l'inclut dans son tableau de dependances. useEffectEvent cree une fonction qui lit toujours les dernieres valeurs reactives sans jamais etre listee comme dependance. L'effet ne se relance donc jamais a cause de cette fonction. Cette distinction est fondamentale : useCallback optimise les re-rendus, useEffectEvent separe la logique reactive de la logique d'action.
Q2 : Un Effect Event peut-il etre passe en prop a un composant enfant ?
Non. Les Effect Events sont strictement reserves a un usage interne au composant ou au hook qui les declare. Leur identite change a chaque rendu, ce qui causerait des re-rendus inutiles si on les transmettait en props. Le plugin ESLint detecte et signale toute tentative de transmission. Ce choix de conception garantit que les Effect Events restent lies au cycle de vie de leur effet parent.
Q3 : Quelle est la difference entre Activity, le rendu conditionnel et CSS display:none ?
Le rendu conditionnel ({show && <Component />}) demonte le composant et detruit son etat. CSS display: none masque le composant visuellement mais maintient tous ses effets actifs, ce qui gaspille des ressources. <Activity mode="hidden"> occupe un terrain intermediaire : l'etat est preserve en memoire, les effets sont nettoyes (timers, WebSockets, subscriptions), les mises a jour sont differees a priorite basse, et le contenu peut etre pre-rendu en arriere-plan.
Q4 : Que devient useEffect dans un Activity masque ?
Lorsqu'un <Activity> passe en mode="hidden", React execute toutes les fonctions de nettoyage des effets (la valeur de retour de useEffect). Aucun nouvel effet n'est monte tant que le composant reste masque. Au retour en mode="visible", les effets se remontent avec l'etat preserve. Cette mecanique explique pourquoi les bibliotheques de data fetching comme TanStack Query, qui reposent sur useEffect en interne, necessitent une strategie de prefetch en dehors de la frontiere Activity.
Q5 : Comment pre-rendre une route avec Activity et Suspense ?
Le pattern consiste a envelopper la route cible dans <Activity mode="hidden"> avec un <Suspense> a l'interieur. Les donnees sont chargees via l'API use() ou une source compatible Suspense. React effectue le rendu de l'arbre masque a basse priorite, resolvant la frontiere Suspense en arriere-plan. Quand l'utilisateur navigue et que le mode bascule a "visible", le contenu deja rendu apparait sans aucun spinner ni etat de chargement.
Q6 : useEffectEvent rend-il obsolete la regle exhaustive-deps du linter ?
Non. La regle exhaustive-deps reste indispensable pour detecter les dependances reellement manquantes. useEffectEvent couvre un cas precis : celui des actions secondaires (analytics, logging, notifications) qui lisent des valeurs reactives sans devoir declencher de resynchronisation. Utiliser systematiquement useEffectEvent pour supprimer les avertissements du linter masquerait des bugs de synchronisation et irait a l'encontre du modele mental de React.
Conclusion
Les points essentiels a retenir sur ces deux APIs de React 19.2 :
- useEffectEvent remplace definitivement le contournement
useRefpour les fermetures obsoletes dans les effets, avec un support natif danseslint-plugin-react-hooks@6+ - Les Effect Events lisent toujours les dernieres valeurs de props et state sans declencher de resynchronisation de l'effet -- a utiliser pour l'analytics, le logging et les callbacks de notification
- Le composant
<Activity>preserve l'etat des composants tout en nettoyant les effets, offrant un compromis optimal entre le rendu conditionnel et le masquage CSS - Les Activity masques effectuent un pre-rendu a priorite basse, ce qui permet une navigation instantanee lorsqu'ils sont combines avec
Suspenseet l'APIuse - TanStack Query et les autres bibliotheques basees sur les effets necessitent un prefetch en dehors des frontieres Activity car
useEffectne s'execute pas en mode masque - Les deux APIs sont disponibles dans React 19.2 -- la mise a jour conjointe d'ESLint et de React est necessaire pour beneficier du support complet des outils
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

React Compiler en 2026 : mémoïsation automatique et questions d'entretien
Analyse approfondie du React Compiler en 2026 : pipeline de compilation, mémoïsation automatique, règles de React et questions posées en entretien technique.

React Server Components en production : patterns et pièges à éviter
React Server Components en production : patterns éprouvés, anti-patterns fréquents et stratégies de débogage pour des applications Next.js 15 robustes.

React 19 : Server Components en production, le guide complet
Maîtrisez les Server Components de React 19 en production. Architecture, patterns, streaming, cache et optimisations pour des applications performantes.