React 19 useEffectEvent und Activity: Neue APIs und Interviewfragen 2026

Ausführlicher Leitfaden zu useEffectEvent und der Activity-Komponente in React 19.2 – mit Codebeispielen, Best Practices und häufigen Interviewfragen für 2026.

React 19 useEffectEvent und Activity API Übersicht

Mit React 19.2 hat das React-Team zwei APIs veröffentlicht, die langjährige Schwachstellen in der Entwicklung adressieren: useEffectEvent beseitigt veraltete Closures in Effects, und <Activity> ermöglicht das Vorrendern im Hintergrund mit vollständiger State-Erhaltung. Beide APIs wurden im Oktober 2025 ausgeliefert und verändern bereits die Art und Weise, wie produktive React-Anwendungen Seiteneffekte und Navigation handhaben.

React 19.2 Mindestversion

useEffectEvent und Activity erfordern React 19.2 oder höher. Das Update erfolgt mit npm install react@latest react-dom@latest. Das ESLint-Plugin eslint-plugin-react-hooks@6+ bietet native Unterstützung für useEffectEvent in Dependency-Arrays.

Das Problem veralteter Closures: Warum useEffectEvent nötig ist

Jede Entwicklerin und jeder Entwickler, der mit React arbeitet, kennt das Problem veralteter Closures. Ein Effect erfasst einen Wert zum Zeitpunkt des Renderns, und wenn sich dieser Wert ändert, referenziert der Effect immer noch den alten. Die klassische Umgehung bestand darin, useRef zu verwenden, um eine mutable Referenz zu halten – funktional, aber umständlich und für den Linter unsichtbar.

Ein typisches Beispiel ist eine Chat-Anwendung, die Analytics protokolliert, wenn eine neue Nachricht eintrifft. Das Log soll das aktuelle theme enthalten, aber Theme-Änderungen sollen die Chat-Verbindung nicht neu aufbauen:

hooks/useChatRoom.tstsx
// Vor useEffectEvent: useRef-Workaround
import { useEffect, useRef } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Theme in einem Ref speichern, um veraltete Closures zu vermeiden
  const themeRef = useRef(theme)
  themeRef.current = theme

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on('message', (msg) => {
      // themeRef.current hat immer den aktuellen Wert
      logAnalytics('new_message', { roomId, theme: themeRef.current })
      showNotification(msg)
    })
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme absichtlich ausgelassen — aber der Linter warnt
}

Dieser Ansatz funktioniert, aber der Linter meldet theme als fehlende Abhängigkeit. Die Warnung zu unterdrücken verbirgt potenzielle Fehler an anderer Stelle. Das useRef-Muster verschleiert zudem die Absicht: Neue Teammitglieder müssen nachvollziehen, warum theme in einem Ref statt im Dependency-Array steht.

useEffectEvent: Reaktive und nicht-reaktive Logik sauber trennen

Der useEffectEvent-Hook erstellt eine stabile Funktion, die stets die neuesten Props und den aktuellen State liest, ohne eine erneute Synchronisierung des Effects auszulösen. Er ersetzt das useRef-Muster durch eine deklarative API, die der Linter nativ versteht.

hooks/useChatRoom.tstsx
// Nach useEffectEvent: saubere Trennung der Zuständigkeiten
import { useEffect, useEffectEvent } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Effect Event: liest immer das aktuelle Theme, löst keinen Reconnect aus
  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]) // Keine Linter-Warnung — onMessage ist ein Effect Event
}

Der Effect wird nur erneut ausgeführt, wenn sich roomId ändert. Der onMessage-Callback sieht immer das aktuelle theme, ohne als Abhängigkeit aufgeführt zu werden. Die offizielle React-Dokumentation stellt ausdrücklich klar, dass Effect Events ausschließlich innerhalb von Effects aufgerufen werden dürfen – niemals beim Rendern oder als Props an Kindkomponenten.

Regeln und Einschränkungen von useEffectEvent

Drei Regeln bestimmen die Verwendung von useEffectEvent:

  1. Aufruf auf der obersten Ebene einer Komponente oder eines Custom Hooks – niemals innerhalb von Schleifen oder Bedingungen
  2. Die zurückgegebene Funktion darf nur innerhalb von useEffect oder einem anderen Effect Event aufgerufen werden
  3. Sie darf nicht als Prop weitergegeben oder von einem Hook für externen Gebrauch zurückgegeben werden

Ein Verstoß gegen Regel 2 verursacht subtile Fehler: Die Funktionsidentität ändert sich bei jedem Render, sodass das Speichern in einem Ref oder die Weitergabe nach unten den Zweck zunichtemacht. Das eslint-plugin-react-hooks@6+ erzwingt diese Einschränkungen automatisch.

components/SearchTracker.tsxtsx
// Korrekt: Effect Event innerhalb von useEffect verwendet
import { useEffect, useEffectEvent, useState } from 'react'

interface SearchTrackerProps {
  query: string
  userId: string
}

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

  // Suchanfragen mit aktuellem Benutzerkontext tracken
  const onSearchComplete = useEffectEvent((resultCount: number) => {
    analytics.track('search_complete', {
      query,
      userId,       // Immer die aktuelle userId
      resultCount,
      timestamp: Date.now(),
    })
  })

  useEffect(() => {
    const controller = new AbortController()
    fetchSearchResults(query, controller.signal).then((data) => {
      setResults(data)
      onSearchComplete(data.length) // Innerhalb von useEffect aufgerufen
    })
    return () => controller.abort()
  }, [query]) // userId sicher ausgelassen — lebt im Effect Event

  return <ResultsList results={results} />
}
Kein Ersatz für den Dependency-Linter

useEffectEvent ist kein Fluchtmechanismus, um den Dependency-Linter stumm zu schalten. Wenn ein Wert tatsächlich steuert, wann ein Effect erneut ausgeführt werden soll, gehört er ins Dependency-Array. Logik sollte nur dann in ein Effect Event ausgelagert werden, wenn sie eine Nebenaktion darstellt (Logging, Benachrichtigungen, Analytics), die reaktive Werte liest, ohne den Effect erneut auslösen zu müssen.

Die Activity-Komponente: Hintergrund-Pre-Rendering mit State-Erhaltung

Die <Activity>-Komponente (ehemals "Offscreen") steuert, ob ihre Kinder sichtbar oder verborgen sind. Im Gegensatz zu konditionellem Rendering, das den State zerstört, oder CSS display: none, das Effects weiterlaufen lässt, erhält <Activity> den State, räumt Effects auf und verschiebt Updates auf Leerlaufpriorität.

components/TabLayout.tsxtsx
// Activity erhält Formular-State beim Tab-Wechsel
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>
  )
}

Wenn Benutzer ein Formular im Tab "Profil" ausfüllen, können sie zum Tab "Einstellungen" wechseln und zurückkehren, ohne Eingaben zu verlieren. Die Effects des verborgenen Tabs (Timer, Subscriptions, Datenabfragen) werden aufgeräumt und Ressourcen freigegeben. Sobald der Tab wieder sichtbar wird, werden die Effects erneut eingehängt und der State sofort wiederhergestellt.

Bereit für deine React / Next.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Activity-Modi: Visible und Hidden im Detail

Die <Activity>-Komponente akzeptiert eine mode-Prop mit zwei Werten:

| Verhalten | mode="visible" | mode="hidden" | |----------|-------------------|------------------| | DOM-Rendering | Normal | display: none per CSS | | Komponenten-State | Aktiv | Im Speicher erhalten | | Effects (useEffect) | Eingehängt | Aufgeräumt | | Update-Priorität | Normal | Auf Leerlauf verschoben | | Pre-Rendering | N/A | Rendering mit niedriger Priorität |

Wenn eine Komponente initial verborgen startet (erstes Rendering mit mode="hidden"), rendert React sie mit niedriger Priorität, ohne Effects einzuhängen. Das ermöglicht sofortige Navigation: Die Zielseite ist bereits im Hintergrund gerendert, wenn der Benutzer klickt.

components/PrerenderedRoute.tsxtsx
// Die nächste wahrscheinliche Route im Hintergrund vorrendern
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() liest das gecachte Promise — funktioniert beim 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>
  )
}

Die verborgene <Activity> rendert das Dashboard mit Leerlaufpriorität vor. In Kombination mit Suspense und der use-API erfolgt das Datenladen im Hintergrund. Wenn isActive auf true wechselt, erscheint der Inhalt sofort ohne Ladeindikator.

Activity und TanStack Query: Der Cache-Fallstrick

Ein häufiger Fehler bei <Activity> betrifft TanStack Query. Da useQuery intern auf useEffect basiert, werden Queries innerhalb einer verborgenen <Activity> nicht ausgeführt – der Effect ist ausgehängt.

components/UserDashboard.tsxtsx
// Problem: useQuery ruft keine Daten ab, wenn Activity verborgen ist
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'

function UserStats() {
  // Diese useQuery wird NICHT laufen, solange sie verborgen ist
  const { data } = useQuery({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })
  return <StatsDisplay data={data} />
}

// Lösung: Daten außerhalb der Activity-Grenze vorladen
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
  const queryClient = useQueryClient()

  // Prefetch auf Elternebene — läuft unabhängig vom Activity-Modus
  queryClient.ensureQueryData({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })

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

Die Lösung ist unkompliziert: Das Vorladen der Daten wird in eine übergeordnete Komponente verlagert, oder es wird queryClient.ensureQueryData außerhalb der <Activity>-Grenze verwendet. Die gecachten Daten stehen dann zur Verfügung, sobald die verborgene Komponente sichtbar wird.

Speicher-Kompromiss

Activity tauscht Speicher gegen Geschwindigkeit. Jeder verborgene Komponentenbaum bleibt mit seinem gesamten DOM im Speicher. Bei Anwendungen mit vielen verborgenen Routen sollte der Speicherverbrauch überwacht werden. Das React-Team arbeitet an einer automatischen Bereinigung der am längsten ungenutzten verborgenen Activities in zukünftigen Versionen.

useEffectEvent und Activity kombinieren

Beide APIs ergänzen sich in realen Navigationsmustern. Ein häufiges Szenario: Ein Dashboard mit Tabs, in dem jeder Tab WebSocket-Subscriptions und Analytics-Tracking aufweist.

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 mit aktueller userId — kein 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]) // Saubere Neuverbindung nur bei Kanalwechsel

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

Beim Kanalwechsel wird die WebSocket-Verbindung des verborgenen Feeds getrennt (Effect-Cleanup durch <Activity>). Der Nachrichtenverlauf bleibt im State erhalten. Das onNewMessage Effect Event stellt sicher, dass Analytics immer die aktuelle userId referenzieren, ohne WebSocket-Neuverbindungen zu erzwingen.

Interviewfragen: useEffectEvent und Activity

Diese Fragen testen das Verständnis der neuen APIs und ihrer Interaktion mit dem Rendering-Modell von React. Sie tauchen zunehmend in React-Interviews bei Unternehmen auf, die React 19.2 einsetzen.

F1: Welches Problem löst useEffectEvent, das useCallback nicht lösen kann?

useCallback erstellt eine memoierte Funktion, die aber trotzdem im Dependency-Array des Effects aufgeführt werden muss. Wenn sich eine ihrer eigenen Abhängigkeiten ändert, ändert sich der Callback, was den Effect erneut auslöst. useEffectEvent erstellt eine Funktion, die stets die neuesten Werte liest, ohne eine Abhängigkeit zu sein – der Effect wird dadurch niemals erneut ausgeführt. Diese Trennung ist mit useCallback allein unmöglich.

F2: Kann ein Effect Event als Prop an eine Kindkomponente weitergegeben werden?

Nein. Effect Events sind dafür konzipiert, ausschließlich innerhalb von useEffect oder anderen Effect Events aufgerufen zu werden. Ihre Identität ändert sich bei jedem Render, sodass die Weitergabe als Props unnötige Re-Renders verursachen und das mentale Modell brechen würde. Das ESLint-Plugin erzwingt diese Regel.

F3: Wie unterscheidet sich Activity von konditionellem Rendering und CSS display:none?

Konditionelles Rendering ({show && <Component />}) unmountet die Komponente vollständig – der State wird zerstört. CSS display: none versteckt visuell, lässt aber alle Effects weiterlaufen und verschwendet Ressourcen. <Activity mode="hidden"> erhält den State, räumt Effects auf, verschiebt Updates auf Leerlaufpriorität und kann Inhalte im Hintergrund vorrendern.

F4: Was passiert mit useEffect innerhalb einer verborgenen Activity?

Wenn eine <Activity> zu mode="hidden" wechselt, führt React alle Cleanup-Funktionen der Effects aus (der Rückgabewert von useEffect). Solange die Komponente verborgen ist, werden keine neuen Effects eingehängt. Wird sie wieder sichtbar, werden die Effects mit dem erhaltenen State erneut eingehängt. Deshalb benötigen Datenlade-Bibliotheken, die auf useEffect basieren, Prefetching-Strategien außerhalb der Activity-Grenze.

F5: Wie lässt sich eine Route mit Activity und Suspense vorrendern?

Die Route wird in <Activity mode="hidden"> mit einer inneren <Suspense>-Boundary eingebettet. Für das Laden der Daten werden die use()-API oder eine Suspense-fähige Datenquelle verwendet. React rendert den verborgenen Baum mit niedriger Priorität und löst die Suspense-Boundary im Hintergrund auf. Wenn der Benutzer navigiert und mode auf "visible" wechselt, erscheint der vollständig gerenderte Inhalt sofort ohne Ladezustand.

F6: Ist useEffectEvent ein Ersatz für die exhaustive-deps-Lint-Regel?

Nein. Die exhaustive-deps-Regel bleibt entscheidend für das Erkennen genuiner fehlender Abhängigkeiten. useEffectEvent behandelt einen spezifischen Fall: Logik, die reaktive Werte liest, aber nicht steuern soll, wann der Effect erneut läuft (Analytics, Benachrichtigungen, Logging). Es zur Unterdrückung aller Dependency-Warnungen zu verwenden, verbirgt Fehler und verfehlt den Zweck.

Fazit

  • useEffectEvent ersetzt den useRef-Workaround für veraltete Closures in Effects, mit nativer Linter-Unterstützung in eslint-plugin-react-hooks@6+
  • Effect Events lesen stets die neuesten Props und den aktuellen State, ohne eine erneute Effect-Synchronisierung auszulösen – ideal für Analytics, Logging und Benachrichtigungs-Callbacks
  • <Activity> erhält den Komponenten-State, während Effects aufgeräumt werden, und bietet einen Mittelweg zwischen konditionellem Rendering und CSS-Verstecken
  • Verborgene Activities rendern mit Leerlaufpriorität vor und ermöglichen sofortige Navigation in Kombination mit Suspense und der use-API
  • TanStack Query und andere Effect-basierte Bibliotheken benötigen Prefetching außerhalb der Activity-Grenzen, da useEffect im verborgenen Modus nicht ausgeführt wird
  • Beide APIs sind ab React 19.2 verfügbar – ESLint und React sollten gemeinsam aktualisiert werden, um die volle Tooling-Unterstützung zu erhalten

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#react
#react-19
#useEffectEvent
#activity
#interview

Teilen

Verwandte Artikel