React 19 useEffectEvent i Activity: nowe API i pytania rekrutacyjne 2026
Kompleksowy przewodnik po useEffectEvent i Activity w React 19.2. Rozwiazywanie stale closures, pre-rendering w tle, przyklady kodu i pytania rekrutacyjne.

React 19.2 wprowadza dwa nowe API, które zmieniają sposób zarządzania efektami ubocznymi i renderowaniem w tle: useEffectEvent oraz Activity. Oba rozwiązują realne problemy znane deweloperom od lat — przestarzałe domknięcia (stale closures) w hookach oraz utratę stanu komponentów przy przełączaniu widoków. W tym artykule omówiono mechanizm działania obu API, przedstawiono praktyczne przykłady kodu oraz zebrano pytania rekrutacyjne, które mogą pojawić się na rozmowach kwalifikacyjnych w 2026 roku.
Hooki useEffectEvent oraz komponent Activity są dostępne od React 19.2. Przed rozpoczęciem pracy z nimi należy upewnić się, że projekt korzysta z odpowiedniej wersji: react@19.2.0 lub nowszej. Pełna dokumentacja zmian dostępna jest w oficjalnym wpisie na blogu React.
Problem przestarzałych domknięć i dotychczasowe obejścia
Jednym z najczęstszych problemów przy pracy z useEffect jest tzw. stale closure — sytuacja, w której callback efektu przechwytuje nieaktualną wartość zmiennej z momentu ostatniego renderowania. Klasycznym przykładem jest hook, który nasłuchuje wiadomości na czacie i loguje je do systemu analityki. Zmiana motywu (theme) nie powinna powodować ponownego połączenia z pokojem czatu, ale jednocześnie logowanie powinno zawsze odzwierciedlać aktualny motyw.
Przed useEffectEvent jedynym rozwiązaniem było użycie useRef jako pośrednika:
// 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
}Powyższe podejście działa, lecz ma istotne wady. Po pierwsze, celowe pomijanie theme w tablicy zależności powoduje ostrzeżenie lintera react-hooks/exhaustive-deps. Po drugie, wzorzec z useRef zaciemnia intencję kodu — czytelnik musi samodzielnie wywnioskować, dlaczego dany ref istnieje i jak jest synchronizowany. W większych bazach kodu prowadzi to do błędów trudnych do wychwycenia podczas code review.
useEffectEvent — czyste rozdzielenie odpowiedzialności
Hook useEffectEvent rozwiązuje problem przestarzałych domknięć na poziomie API. Pozwala zdefiniować funkcję, która zawsze odczytuje najnowsze wartości propsów i stanu, ale nigdy nie powoduje ponownego uruchomienia efektu. Semantycznie jest to "zdarzenie wywoływane przez efekt" — nie jest to sam efekt, lecz reakcja na coś, co dzieje się wewnątrz efektu.
// 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
}Różnica jest fundamentalna. Tablica zależności [roomId] jest teraz kompletna i poprawna — linter nie zgłasza żadnych ostrzeżeń. Zmienna theme jest odczytywana wewnątrz useEffectEvent, więc zawsze ma aktualną wartość bez konieczności stosowania refów. Kod jasno komunikuje intencję: połączenie zależy od roomId, a logowanie analityki to zdarzenie, które korzysta z bieżącego kontekstu.
Zasady i ograniczenia useEffectEvent
Hook useEffectEvent podlega ścisłym regułom, których naruszenie prowadzi do błędów w czasie wykonania lub ostrzeżeń lintera. Znajomość tych reguł jest kluczowa zarówno w codziennej pracy, jak i na rozmowach rekrutacyjnych.
Funkcja zwrócona przez useEffectEvent może być wywoływana wyłącznie z wnętrza useEffect. Nie wolno przekazywać jej jako propsu do komponentów potomnych ani wywoływać w handlerach zdarzeń DOM. Jest to celowe ograniczenie — API zostało zaprojektowane wyłącznie do rozwiązywania problemu przestarzałych domknięć w efektach.
Poniższy przykład ilustruje poprawne użycie — onSearchComplete jest wywoływany wewnątrz useEffect, a zmienna userId jest zawsze aktualna:
// 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} />
}Funkcji zwróconej przez useEffectEvent nie wolno przekazywać jako prop do komponentów potomnych, wywoływać w obsłudze zdarzeń onClick/onChange ani używać poza kontekstem useEffect. Naruszenie tych reguł prowadzi do nieprzewidywalnego zachowania aplikacji. Pełna specyfikacja ograniczeń dostępna jest w dokumentacji React.
Komponent Activity — renderowanie w tle bez utraty stanu
Drugim nowym API w React 19.2 jest komponent Activity (wcześniej znany pod roboczą nazwą Offscreen). Rozwiązuje on problem, z którym deweloperzy borykają się od lat: jak ukryć fragment interfejsu bez odmontowywania go z drzewa komponentów i utraty wewnętrznego stanu.
Typowy scenariusz to układ z zakładkami, w którym użytkownik wypełnia formularz w jednej zakładce, przełącza się na drugą, a po powrocie oczekuje, że formularz zachował wprowadzone dane. Bez Activity realizacja tego wymaga ręcznego zarządzania stanem na poziomie rodzica lub użycia CSS display: none, co nie jest optymalne z perspektywy wydajności.
Tryby Activity: visible i hidden
Komponent Activity przyjmuje prop mode z dwiema wartościami: 'visible' oraz 'hidden'. W trybie 'visible' drzewo potomne renderuje się i zachowuje normalnie. W trybie 'hidden' React zachowuje cały stan komponentów w pamięci, ale wstrzymuje efekty uboczne — useEffect cleanup uruchamia się przy przejściu do 'hidden', a setup ponownie przy powrocie do 'visible'.
// 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>
)
}Warto zwrócić uwagę na kluczowy szczegół implementacyjny: każda zakładka jest renderowana od razu, ale tylko aktywna ma tryb 'visible'. Przełączanie zakładek zmienia jedynie prop mode, nie odmontowuje komponentów. Dzięki temu stan formularzy, pozycja scrolla i lokalne zmienne stanu są zachowane.
Komponent Activity umożliwia również pre-rendering — wstępne renderowanie tras lub widoków, do których użytkownik prawdopodobnie nawiguje w następnym kroku:
// 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>
)
}W tym wzorcu Suspense współpracuje z Activity — obietnica (promise) jest rozwiązywana w tle, a gdy użytkownik przejdzie do danej trasy, treść wyświetla się natychmiast, bez widocznego opóźnienia.
Gotowy na rozmowy o React / Next.js?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Pułapka z TanStack Query i Activity
Integracja Activity z bibliotekami zarządzania stanem serwerowym, takimi jak TanStack Query, wymaga szczególnej uwagi. Gdy komponent znajduje się wewnątrz Activity w trybie 'hidden', React wstrzymuje jego efekty uboczne. Oznacza to, że useQuery nie uruchomi zapytania sieciowego, ponieważ wewnętrznie korzysta z useEffect do inicjalizacji fetchingu.
// 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>
)
}Rozwiązaniem jest przeniesienie logiki prefetchingu na poziom komponentu nadrzędnego za pomocą queryClient.ensureQueryData. Ta metoda działa poza granicą Activity, więc dane są pobierane niezależnie od trybu. Gdy Activity przejdzie do 'visible', useQuery wewnątrz UserStats odczyta dane z cache TanStack Query bez dodatkowego zapytania sieciowego. Więcej o zaawansowanych wzorcach TanStack Query można przeczytać w sekcji pytania rekrutacyjne React Query.
Metoda ensureQueryData pobiera dane tylko wtedy, gdy nie ma ich jeszcze w cache (lub są przestarzałe zgodnie z staleTime). W odróżnieniu od prefetchQuery, zwraca ona Promise z danymi, co pozwala na ich użycie również w logice rodzica. W kontekście Activity oba podejścia są poprawne, ale ensureQueryData jest bardziej wydajne, ponieważ nie wykonuje zbędnych zapytań.
Połączenie useEffectEvent i Activity — praktyczny wzorzec
Oba API najlepiej prezentują swoje możliwości, gdy są używane razem. Poniższy przykład przedstawia dashboard z kanałami na żywo — każdy kanał utrzymuje połączenie WebSocket, a przełączanie między kanałami nie powoduje utraty wiadomości ani niepotrzebnego reconnectu:
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>
)
}W tym wzorcu zachodzą trzy kluczowe mechanizmy jednocześnie. Po pierwsze, useEffectEvent gwarantuje, że userId w logowaniu analityki jest zawsze aktualny, bez ponownego otwierania połączenia WebSocket. Po drugie, Activity zachowuje stan wiadomości w ukrytych kanałach — przełączenie z powrotem na kanał natychmiast wyświetla pełną historię. Po trzecie, cleanup efektu (zamknięcie WebSocket) uruchamia się automatycznie, gdy Activity przechodzi do trybu 'hidden', co oszczędza zasoby sieciowe. Przy powrocie do 'visible' efekt uruchamia się ponownie, nawiązując świeże połączenie.
Pytania rekrutacyjne — useEffectEvent i Activity
Poniższe pytania regularnie pojawiają się na rozmowach kwalifikacyjnych w 2026 roku. Kandydaci powinni znać nie tylko odpowiedzi, ale również potrafić uzasadnić je przykładami kodu.
1. Czym różni się useEffectEvent od zwykłego callbacka przekazanego do useEffect?
Zwykły callback przechwytuje wartości z momentu ostatniego renderowania, w którym efekt się uruchomił. useEffectEvent zawsze odczytuje najnowsze wartości propsów i stanu, nie będąc częścią tablicy zależności efektu.
2. Dlaczego nie wolno przekazywać funkcji z useEffectEvent jako propsu?
Funkcja z useEffectEvent jest powiązana z cyklem życia efektu — jest stabilna referencyjnie, ale jej semantyka zakłada wywołanie wyłącznie z wnętrza useEffect. Przekazanie jej jako propsu złamałoby tę gwarancję i mogłoby prowadzić do nieprzewidywalnego zachowania.
3. Co dzieje się z useEffect wewnątrz Activity w trybie hidden?
React uruchamia cleanup efektów przy przejściu do 'hidden' i ponownie setup przy powrocie do 'visible'. Stan komponentu (useState, useRef) jest zachowany w pamięci, ale efekty uboczne są wstrzymane.
4. Jak rozwiązać problem z useQuery wewnątrz Activity?
Należy przenieść prefetching danych na poziom komponentu nadrzędnego za pomocą queryClient.ensureQueryData lub queryClient.prefetchQuery. Te metody działają poza granicą Activity i zapełniają cache, z którego useQuery odczyta dane po przejściu do trybu 'visible'.
5. Czy Activity zastępuje React.memo lub useMemo?
Nie. Activity rozwiązuje problem zachowania stanu ukrytych komponentów, podczas gdy React.memo i useMemo optymalizują ponowne renderowanie. Mogą być stosowane razem — np. komponent wewnątrz Activity może używać useMemo do optymalizacji obliczeń.
6. Podaj scenariusz, w którym useEffectEvent i Activity współpracują.
Dashboard z wieloma kanałami WebSocket, gdzie przełączanie między kanałami nie powoduje utraty wiadomości. Activity zachowuje stan wiadomości, useEffectEvent zapewnia aktualne dane w logowaniu analityki bez ponownego nawiązywania połączeń.
7. Co się stanie, jeśli wywołasz funkcję z useEffectEvent poza useEffect? React zgłosi błąd lub ostrzeżenie w trybie deweloperskim. API zostało zaprojektowane wyłącznie do użytku wewnątrz efektów i naruszenie tej reguły jest traktowane jako błąd programistyczny.
Więcej pytań rekrutacyjnych dotyczących hooków React dostępnych jest w sekcji pytania rekrutacyjne React Hooks.
Podsumowanie
useEffectEvent i Activity to dwa uzupełniające się API, które adresują realne problemy w aplikacjach React. Pierwsze eliminuje przestarzałe domknięcia w efektach bez konieczności stosowania wzorca z useRef, drugie umożliwia zachowanie stanu komponentów w tle i pre-rendering tras. W połączeniu dają potężny zestaw narzędzi do budowy responsywnych interfejsów z zachowaniem pełnej kontroli nad cyklem życia komponentów i efektów ubocznych. Znajomość obu API, ich ograniczeń oraz wzorców integracji z bibliotekami takimi jak TanStack Query stanowi istotny element przygotowania do rozmów rekrutacyjnych na stanowiska React w 2026 roku.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Cache Components w Next.js 16: use cache, PPR i pytania rekrutacyjne na 2026 rok
Kompletny przewodnik po Cache Components w Next.js 16: dyrektywa use cache, Partial Pre-Rendering, cacheLife, cacheTag, bezpieczeństwo z use cache private oraz pytania na rozmowę kwalifikacyjną.

React Compiler w 2026: automatyczna memoizacja i pytania rekrutacyjne
Kompletny przewodnik po React Compiler — automatyczna memoizacja, pipeline kompilacji, reguły React, integracja z ESLint i pytania na rozmowy kwalifikacyjne dla React w 2026 roku.

React Server Components na produkcji: wzorce i pułapki
React Server Components na produkcji: sprawdzone wzorce, częste antywzorce i strategie debugowania dla solidnych aplikacji Next.js 15.