React 19 useEffectEvent en Activity: Nieuwe API's en Sollicitatievragen 2026

Uitgebreide gids over useEffectEvent en het Activity-component in React 19.2 – met codevoorbeelden, best practices en veelgestelde sollicitatievragen voor 2026.

Overzicht van de useEffectEvent en Activity API's in React 19

React 19.2 heeft twee API's ge\u00efntroduceerd die al lang bestaande pijnpunten aanpakken: useEffectEvent elimineert stale closures in effects, en <Activity> maakt achtergrond pre-rendering mogelijk met volledige state-behoud. Beide API's zijn uitgebracht in oktober 2025 en veranderen nu al hoe productie React-applicaties side effects en navigatie afhandelen.

Minimaal React 19.2

useEffectEvent en Activity vereisen React 19.2 of hoger. Update met npm install react@latest react-dom@latest. De ESLint-plugin eslint-plugin-react-hooks@6+ voegt native ondersteuning toe voor useEffectEvent in dependency arrays.

Het stale closure-probleem: waarom useEffectEvent nodig is

Iedere React-ontwikkelaar heeft wel eens te maken gehad met stale closures. Een effect legt een waarde vast op het moment van rendering, en wanneer die waarde verandert, verwijst het effect nog steeds naar de oude. De klassieke workaround was het gebruik van useRef om een muteerbare referentie vast te houden \u2014 werkend, maar omslachtig en onzichtbaar voor de linter.

Neem een chatapplicatie die analytics registreert wanneer een nieuw bericht binnenkomt. Het log moet het huidige theme bevatten, maar theme-wijzigingen mogen de chatverbinding niet opnieuw opzetten:

hooks/useChatRoom.tstsx
// V\u00f3\u00f3r useEffectEvent: useRef-workaround
import { useEffect, useRef } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Theme opslaan in een ref om stale closure te voorkomen
  const themeRef = useRef(theme)
  themeRef.current = theme

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on('message', (msg) => {
      // themeRef.current heeft altijd de actuele waarde
      logAnalytics('new_message', { roomId, theme: themeRef.current })
      showNotification(msg)
    })
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme bewust weggelaten — maar de linter waarschuwt
}

Deze aanpak werkt, maar de linter markeert theme als een ontbrekende dependency. Het onderdrukken van de waarschuwing verbergt potenti\u00eble bugs elders. Het useRef-patroon verhult ook de intentie: een nieuwe ontwikkelaar die deze code leest, moet mentaal reconstrueren waarom theme in een ref zit in plaats van in de dependency array.

useEffectEvent: reactieve en niet-reactieve logica scheiden

De useEffectEvent-hook cre\u00ebert een stabiele functie die altijd de meest recente props en state leest, zonder de effect-resynchronisatie te activeren. Het vervangt het useRef-patroon door een declaratieve API die de linter native begrijpt.

hooks/useChatRoom.tstsx
// Na useEffectEvent: schone scheiding van verantwoordelijkheden
import { useEffect, useEffectEvent } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Effect Event: leest altijd het huidige theme, veroorzaakt geen 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]) // Geen linter-waarschuwing — onMessage is een Effect Event
}

Het effect wordt alleen opnieuw uitgevoerd wanneer roomId verandert. De onMessage-callback ziet altijd het huidige theme zonder als dependency te worden vermeld. De offici\u00eble React-documentatie stelt expliciet dat Effect Events alleen vanuit effects mogen worden aangeroepen \u2014 nooit tijdens rendering of als props doorgegeven aan child-componenten.

Regels en beperkingen van useEffectEvent

Drie regels bepalen het gebruik van useEffectEvent:

  1. Aanroepen op het hoogste niveau van een component of custom hook \u2014 nooit binnen loops of condities
  2. De geretourneerde functie mag alleen worden aangeroepen vanuit useEffect of een ander Effect Event
  3. De functie mag niet als prop worden doorgegeven of worden geretourneerd door een hook voor extern gebruik

Het overtreden van regel 2 veroorzaakt subtiele bugs: de functie-identiteit verandert bij elke render, dus het opslaan in een ref of doorgeven naar beneden ondermijnt het doel. De eslint-plugin-react-hooks@6+ handhaaft deze beperkingen automatisch.

components/SearchTracker.tsxtsx
// Correct: Effect Event gebruikt binnen useEffect
import { useEffect, useEffectEvent, useState } from 'react'

interface SearchTrackerProps {
  query: string
  userId: string
}

export function SearchTracker({ query, userId }: SearchTrackerProps) {
  const [results, setResults] = useState<string[]>([])

  // Zoekopdrachten bijhouden met de huidige gebruikerscontext
  const onSearchComplete = useEffectEvent((resultCount: number) => {
    analytics.track('search_complete', {
      query,
      userId,       // Altijd de actuele userId
      resultCount,
      timestamp: Date.now(),
    })
  })

  useEffect(() => {
    const controller = new AbortController()
    fetchSearchResults(query, controller.signal).then((data) => {
      setResults(data)
      onSearchComplete(data.length) // Aangeroepen vanuit useEffect
    })
    return () => controller.abort()
  }, [query]) // userId veilig weggelaten — zit in het Effect Event

  return <ResultsList results={results} />
}
Geen vervanging voor de dependency-linter

useEffectEvent is geen ontsnappingsroute om de dependency-linter het zwijgen op te leggen. Als een waarde daadwerkelijk bepaalt wanneer een effect opnieuw moet worden uitgevoerd, hoort deze in de dependency array. Logica hoort alleen in een Effect Event te worden ge\u00ebxtraheerd wanneer het een nevenactie betreft (logging, notificaties, analytics) die reactieve waarden leest zonder het effect opnieuw te hoeven activeren.

Het Activity-component: achtergrond pre-rendering met state-behoud

Het <Activity>-component (voorheen "Offscreen" genoemd) bepaalt of zijn children zichtbaar of verborgen zijn. In tegenstelling tot conditioneel rendering dat state vernietigt, of CSS display: none dat effects laat draaien, behoudt <Activity> de state terwijl het effects opruimt en updates uitstelt.

components/TabLayout.tsxtsx
// Activity behoudt formulier-state bij tab-wisseling
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>
  )
}

Wanneer een gebruiker een formulier invult op het tabblad "Profiel", kan deze overschakelen naar "Instellingen" en terugkeren zonder invoer te verliezen. De effects van het verborgen tabblad (timers, subscriptions, data fetching) worden opgeruimd en resources worden vrijgegeven. Wanneer het tabblad weer zichtbaar wordt, worden effects opnieuw gemount en wordt de state direct hersteld.

Klaar om je React / Next.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Activity-modi: visible en hidden onder de motorkap

Het <Activity>-component accepteert een mode-prop met twee waarden:

| Gedrag | mode="visible" | mode="hidden" | |----------|-------------------|------------------| | DOM-rendering | Normaal | display: none via CSS | | Component-state | Actief | Behouden in geheugen | | Effects (useEffect) | Gemount | Opgeruimd | | Update-prioriteit | Normaal | Uitgesteld naar idle | | Pre-rendering | N.v.t. | Rendering met lage prioriteit |

Wanneer een component verborgen start (eerste render met mode="hidden"), rendert React het met lage prioriteit zonder effects te mounten. Dit maakt directe navigatie mogelijk: de doelpagina is al op de achtergrond gerenderd wanneer de gebruiker klikt.

components/PrerenderedRoute.tsxtsx
// De volgende waarschijnlijke route op de achtergrond pre-renderen
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() leest de gecachte promise — werkt tijdens verborgen 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>
  )
}

De verborgen <Activity> pre-rendert het dashboard op idle-prioriteit. Gecombineerd met Suspense en de use-API vindt het laden van data plaats op de achtergrond. Wanneer isActive overschakelt naar true, verschijnt de inhoud direct zonder laadindicator.

Activity en TanStack Query: de cache-valkuil

Een veelvoorkomende fout met <Activity> betreft TanStack Query. Aangezien useQuery intern afhankelijk is van useEffect, worden queries binnen een verborgen <Activity> niet uitgevoerd \u2014 het effect is gedemount.

components/UserDashboard.tsxtsx
// Probleem: useQuery haalt geen data op wanneer Activity verborgen is
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'

function UserStats() {
  // Deze useQuery zal NIET draaien terwijl het verborgen is
  const { data } = useQuery({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })
  return <StatsDisplay data={data} />
}

// Oplossing: data prefetchen buiten de Activity-grens
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
  const queryClient = useQueryClient()

  // Prefetch op ouderniveau — draait ongeacht de Activity-modus
  queryClient.ensureQueryData({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })

  return (
    <Activity mode={showStats ? 'visible' : 'hidden'}>
      <UserStats />
    </Activity>
  )
}

De oplossing is eenvoudig: verplaats het prefetchen van data naar een oudercomponent of gebruik queryClient.ensureQueryData buiten de <Activity>-grens. De gecachte data is beschikbaar zodra het verborgen component weer zichtbaar wordt.

Geheugenafweging

Activity ruilt geheugen voor snelheid. Elke verborgen componentenboom blijft in het geheugen met zijn volledige DOM. Bij applicaties met veel verborgen routes is het raadzaam het geheugengebruik te monitoren. Het React-team onderzoekt automatische verwijdering van de minst recent gebruikte verborgen Activities in toekomstige versies.

useEffectEvent en Activity combineren

Beide API's vullen elkaar aan in realistische navigatiepatronen. Een veelvoorkomend scenario: een dashboard met tabbladen waarbij elk tabblad WebSocket-subscriptions en analytics-tracking heeft.

components/LiveDashboard.tsxtsx
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 met huidige userId — geen effect-resync
  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]) // Schone herverbinding alleen bij kanaaalwissel

  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>
  )
}

Bij het wisselen van kanaal wordt de WebSocket-verbinding van de verborgen feed verbroken (effect-cleanup via <Activity>). De berichtengeschiedenis blijft behouden in de state. Het onNewMessage Effect Event zorgt ervoor dat analytics altijd naar de huidige userId verwijzen zonder WebSocket-herverbindingen af te dwingen.

Sollicitatievragen: useEffectEvent en Activity

Deze vragen toetsen het begrip van de nieuwe API's en hun interactie met het renderingmodel van React. Ze komen steeds vaker voor in React-sollicitatiegesprekken bij bedrijven die React 19.2 gebruiken.

V1: Welk probleem lost useEffectEvent op dat useCallback niet kan oplossen?

useCallback cre\u00ebert een gememoriseerde functie, maar die moet nog steeds in de dependency array van het effect worden opgenomen. Als een van de eigen dependencies verandert, verandert de callback, wat het effect opnieuw activeert. useEffectEvent cre\u00ebert een functie die altijd de meest recente waarden leest zonder een dependency te zijn \u2014 het effect wordt er nooit door opnieuw uitgevoerd. Deze scheiding is met useCallback alleen onmogelijk.

V2: Kan een Effect Event als prop aan een child-component worden doorgegeven?

Nee. Effect Events zijn ontworpen om uitsluitend vanuit useEffect of andere Effect Events te worden aangeroepen. Hun identiteit verandert bij elke render, dus het doorgeven als props zou onnodige re-renders veroorzaken en het mentale model doorbreken. De ESLint-plugin handhaaft deze regel.

V3: Hoe verschilt Activity van conditioneel rendering en CSS display:none?

Conditioneel rendering ({show && <Component />}) demount het component volledig \u2014 de state wordt vernietigd. CSS display: none verbergt visueel maar houdt alle effects draaiend, wat resources verspilt. <Activity mode="hidden"> behoudt de state, ruimt effects op, stelt updates uit naar idle-prioriteit en kan content op de achtergrond pre-renderen.

V4: Wat gebeurt er met useEffect binnen een verborgen Activity?

Wanneer een <Activity> overschakelt naar mode="hidden", voert React alle cleanup-functies van effects uit (de retourwaarde van useEffect). Geen nieuwe effects worden gemount zolang het component verborgen is. Wanneer het weer zichtbaar wordt, worden effects opnieuw gemount met de behouden state. Daarom hebben data fetching-bibliotheken die op useEffect vertrouwen prefetching-strategie\u00ebn nodig buiten de Activity-grens.

V5: Hoe kan een route worden ge-pre-renderd met Activity en Suspense?

De route wordt gewrapped in <Activity mode="hidden"> met een <Suspense>-boundary erbinnen. Voor het laden van data wordt de use()-API of een Suspense-compatibele databron gebruikt. React rendert de verborgen boom op lage prioriteit en lost de Suspense-boundary op de achtergrond op. Wanneer de gebruiker navigeert en mode overschakelt naar "visible", verschijnt de volledig gerenderde content direct zonder laadstatus.

V6: Is useEffectEvent een vervanging voor de exhaustive-deps lint-regel?

Nee. De exhaustive-deps-regel blijft essentieel voor het opsporen van daadwerkelijk ontbrekende dependencies. useEffectEvent behandelt een specifiek geval: logica die reactieve waarden leest maar niet mag bepalen wanneer het effect opnieuw draait (analytics, notificaties, logging). Het gebruiken om alle dependency-waarschuwingen te onderdrukken verbergt bugs en ondermijnt het doel.

Conclusie

  • useEffectEvent vervangt de useRef-workaround voor stale closures in effects, met native linter-ondersteuning in eslint-plugin-react-hooks@6+
  • Effect Events lezen altijd de meest recente props en state zonder effect-resynchronisatie te activeren \u2014 ideaal voor analytics, logging en notificatie-callbacks
  • <Activity> behoudt component-state terwijl effects worden opgeruimd, als middenweg tussen conditioneel rendering en CSS-verberging
  • Verborgen Activities pre-renderen op idle-prioriteit, wat directe navigatie mogelijk maakt in combinatie met Suspense en de use-API
  • TanStack Query en andere effect-gebaseerde bibliotheken hebben prefetching nodig buiten Activity-grenzen, aangezien useEffect niet draait in verborgen modus
  • Beide API's zijn beschikbaar vanaf React 19.2 \u2014 ESLint en React samen updaten voor volledige tooling-ondersteuning

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#react
#react-19
#useEffectEvent
#activity
#sollicitatie

Delen

Gerelateerde artikelen