# 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. - Published: 2026-06-03 - Updated: 2026-06-03 - Author: SharpSkill - Tags: react, useEffectEvent, activity, react 19, hooks, interview - Reading time: 11 min --- 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. > **React 19.2 wymagany** > > 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](https://react.dev/blog/2025/10/01/react-19-2). ## 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: ```tsx // hooks/useChatRoom.ts // 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. ```tsx // hooks/useChatRoom.ts // 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: ```tsx // components/SearchTracker.tsx // 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([]) // 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 } ``` > **Czego nie wolno robić z useEffectEvent** > > 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](https://react.dev/reference/react/useEffectEvent). ## 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'`. ```tsx // components/TabLayout.tsx // 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 (
{tabs.map((tab) => ( {tab.content} ))}
) } ``` 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: ```tsx // components/PrerenderedRoute.tsx // Pre-render the next likely route in the background import { Activity, Suspense, use } from 'react' interface PrerenderedRouteProps { isActive: boolean dataPromise: Promise } export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) { return ( }> ) } function DashboardContent({ dataPromise }: { dataPromise: Promise }) { // use() reads the cached promise — works during hidden pre-render const data = use(dataPromise) return (
) } ``` 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. ## 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. ```tsx // components/UserDashboard.tsx // 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 } // 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 ( ) } ``` 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](/technologies/react-next/interview-questions/react-query-tanstack). > **ensureQueryData vs prefetchQuery** > > 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: ```tsx // components/LiveDashboard.tsx import { useEffect, useEffectEvent, useState } from 'react' import { Activity } from 'react' function LiveFeed({ channel, userId }: { channel: string; userId: string }) { const [messages, setMessages] = useState([]) // 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 } export function LiveDashboard({ userId }: { userId: string }) { const [activeChannel, setActiveChannel] = useState('general') const channels = ['general', 'alerts', 'metrics'] return (
{channels.map((ch) => ( ))}
) } ``` 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](/technologies/react-next/interview-questions/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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/react-next/react-19-useeffectevent-activity-new-apis-interview-questions