# React 19 useEffectEvent ta Activity: novi API ta pytannya na spivbesidi 2026 > Detalnyi ohliad useEffectEvent ta Activity v React 19.2. Vyrishennia stale closures, fоnovyi pre-rendering, pryklady kodu ta pytannya na tekhnichni spivbesidy. - Published: 2026-06-03 - Updated: 2026-06-03 - Author: SharpSkill - Tags: react, useEffectEvent, activity, react 19, hooks, interview - Reading time: 11 min --- React 19 представив два експериментальні API, які змінюють підхід до роботи з побічними ефектами та управління станом компонентів: `useEffectEvent` та `Activity`. Перший вирішує давню проблему застарілих замикань (stale closures) в обробниках подій всередині `useEffect`, а другий дозволяє зберігати стан прихованих компонентів без їх повного розмонтування. Ці API вже доступні в [React 19.2](https://react.dev/blog/2025/10/01/react-19-2) та активно тестуються в production-проєктах. У цій статті розглядаються обидва API з прикладами коду, типовими помилками та питаннями, які зустрічаються на технічних співбесідах у 2026 році. > **Вимоги до версії** > > `useEffectEvent` доступний починаючи з React 19.2 та потребує прапорця `experimental`. `Activity` (раніше відомий як `Offscreen`) знаходиться в експериментальній фазі та доступний через canary-збірки React. Обидва API можуть змінити свій інтерфейс до стабільного релізу. ## Проблема застарілих замикань у useEffect Одна з найпоширеніших помилок у React-додатках — використання застарілих значень стану або пропсів всередині колбеків `useEffect`. Коли розробник підписується на подію (WebSocket, таймер, зовнішній listener) і використовує в обробнику значення з пропсів, виникає дилема: або додати це значення до масиву залежностей (що спричинить небажане перепідключення), або використати `useRef` як обхідне рішення. Класичний приклад — чат-компонент, де підключення до кімнати має відбуватися лише при зміні `roomId`, але обробник повідомлень потребує актуального значення `theme` для аналітики: ```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 } ``` Цей патерн працює, але має суттєві недоліки. Лінтер React Hooks попереджає про відсутність `theme` в масиві залежностей. Ручне управління рефами порушує декларативну модель React і ускладнює код-ревʼю. У великих кодових базах такі обхідні рішення множаться і стають джерелом багів. ## useEffectEvent: чистий розподіл відповідальностей Хук [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) створює стабільну функцію, яка завжди має доступ до актуальних значень пропсів і стану, але при цьому не входить до системи залежностей `useEffect`. React гарантує, що ідентичність цієї функції залишається незмінною між рендерами. ```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 } ``` Різниця очевидна: `theme` читається безпосередньо в тілі Effect Event без рефів, лінтер не генерує попереджень, а перепідключення до кімнати відбувається лише при зміні `roomId`. Код стає чистішим і зрозумілішим. ## Правила та обмеження useEffectEvent Перш ніж використовувати `useEffectEvent` у проєкті, необхідно розуміти його обмеження. React накладає суворі правила на цей хук, щоб забезпечити передбачувану поведінку. **Виклик лише з useEffect.** Effect Event можна викликати тільки зсередини `useEffect`. Виклик з обробників подій JSX, з тіла компонента або з інших хуків призведе до помилки. **Заборона передачі в інші компоненти.** Функцію, створену через `useEffectEvent`, не можна передавати як проп дочірньому компоненту. Вона призначена виключно для локального використання всередині того ефекту, де вона потрібна. **Один Effect Event на одну логічну дію.** Рекомендується створювати окремий Effect Event для кожної окремої дії (аналітика, нотифікація, логування), а не об'єднувати різну логіку в одну функцію. Нижче наведено коректний приклад використання Effect Event для трекінгу пошукових запитів: ```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 } ``` У цьому прикладі `userId` може змінюватися (наприклад, при перемиканні акаунтів), але ефект пошуку перезапускається лише при зміні `query`. Аналітика при цьому завжди отримує актуальний `userId`. ## Компонент Activity: збереження стану прихованих компонентів Другий новий API — `Activity` — вирішує іншу проблему. У традиційному React, коли компонент прибирається з DOM (наприклад, при перемиканні вкладок), увесь його стан втрачається. Форми очищуються, скрол-позиція скидається, підписки розриваються. `Activity` дозволяє приховати компонент, зберігши його стан у пам'яті. ```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} ))}
) } ``` `Activity` приймає проп `mode` з двома значеннями: `'visible'` та `'hidden'`. У режимі `hidden` компонент залишається у React-дереві, але не рендериться в DOM. Усі `useState`, контекст та рефи зберігаються. При поверненні до `'visible'` компонент відновлюється миттєво, без повторної ініціалізації. ## Передрендеринг маршрутів з Activity Окрім збереження стану, `Activity` відкриває можливість попереднього рендерингу (pre-rendering) контенту у фоновому режимі. Це корисно для дешбордів, де наступний маршрут можна підготувати заздалегідь: ```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 (
) } ``` Хук `use()` читає закешований проміс навіть під час прихованого рендерингу. Коли користувач переходить на цей маршрут, дані вже готові, і компонент відображається миттєво. ## Підводний камінь: Activity та TanStack Query При використанні `Activity` разом із [TanStack Query](/technologies/react-next/interview-questions/react-query-tanstack) виникає неочевидна проблема. Коли компонент знаходиться в режимі `hidden`, хуки всередині нього не виконуються. Це означає, що `useQuery` не ініціює запит до API, і при перемиканні на видимий режим дані можуть бути відсутні. > **useQuery не виконується в прихованому Activity** > > Компоненти в режимі `hidden` не запускають свої ефекти та хуки. Якщо `useQuery` знаходиться всередині `Activity` з `mode="hidden"`, дані не завантажуються. Необхідно виконувати prefetch на рівні батьківського компонента. ```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 ( ) } ``` Рішення полягає у використанні `queryClient.ensureQueryData()` або `queryClient.prefetchQuery()` на рівні батьківського компонента, який завжди залишається видимим. Таким чином, дані потраплять у кеш TanStack Query незалежно від стану `Activity`, а `useQuery` всередині прихованого компонента підхопить їх із кешу при переході в режим `visible`. ## Поєднання useEffectEvent та Activity Найцікавіший сценарій виникає при комбінуванні обох API. Розглянемо live-дешборд із кількома каналами WebSocket, де кожен канал працює в окремій вкладці: ```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) => ( ))}
) } ``` У цьому прикладі `useEffectEvent` забезпечує актуальність `userId` в аналітиці без перепідключення WebSocket, а `Activity` зберігає стан повідомлень кожного каналу при перемиканні між вкладками. При переході на прихований канал WebSocket-з'єднання зупиняється (ефекти не працюють у `hidden`), а при поверненні — відновлюється. Історія повідомлень при цьому залишається на місці. > **Поведінка ефектів у hidden-режимі** > > Коли `Activity` переходить у режим `hidden`, React виконує cleanup-функції всіх `useEffect` всередині. При поверненні в `visible` ефекти запускаються заново. Це означає, що WebSocket-з'єднання коректно закривається та відкривається, але стан `useState` зберігається. ## Питання для технічних співбесід Ці API вже з'являються на [технічних співбесідах з React](/technologies/react-next/interview-questions/react-hooks) у 2026 році. Нижче наведено типові питання та ключові тези для відповідей. **Яку проблему вирішує useEffectEvent?** Хук вирішує проблему застарілих замикань (stale closures) в обробниках подій усередині `useEffect`. Він дозволяє читати актуальні значення пропсів і стану без додавання їх до масиву залежностей ефекту, що запобігає небажаним перезапускам ефектів. **Чим useEffectEvent відрізняється від useCallback?** `useCallback` повертає мемоізовану функцію, яка оновлюється при зміні залежностей і входить до реактивної системи React. `useEffectEvent` повертає стабільну функцію, яка завжди має доступ до актуальних значень, але ніколи не спричиняє перезапуск ефекту. Effect Event навмисно виключений із системи залежностей. **Чи можна передати Effect Event як проп дочірньому компоненту?** Ні. React забороняє це. Effect Event призначений для локального використання всередині компонента, де він був створений, та може викликатися лише з `useEffect`. **Чим Activity відрізняється від умовного рендерингу?** При умовному рендерингу (`{show && }`) компонент повністю розмонтується та втрачає стан. `Activity` з `mode="hidden"` прибирає компонент з DOM, але зберігає увесь його React-стан (useState, контекст, рефи). При поверненні в `visible` стан відновлюється без повторної ініціалізації. **Що відбувається з useEffect всередині прихованого Activity?** React виконує cleanup-функції всіх ефектів при переході в `hidden` та перезапускає ефекти при поверненні в `visible`. Це забезпечує коректне управління ресурсами (підписки, WebSocket-з'єднання), при цьому стан компонента зберігається. **Як вирішити проблему з TanStack Query всередині Activity?** `useQuery` не виконує запити у прихованому `Activity`. Рішення — використовувати `queryClient.ensureQueryData()` або `queryClient.prefetchQuery()` у батьківському компоненті, що знаходиться за межами `Activity`. Дані потраплять у кеш і будуть доступні для `useQuery` при переході компонента у видимий режим. **Як useEffectEvent та Activity працюють разом?** `useEffectEvent` забезпечує актуальність значень у колбеках ефектів, а `Activity` зберігає стан компонентів між перемиканнями. Разом вони дозволяють створювати інтерфейси з кількома вкладками, де кожна вкладка має свій стан і підписки, які коректно зупиняються та відновлюються без втрати даних. ## Висновки `useEffectEvent` та `Activity` заповнюють важливі прогалини в API React. Перший усуває необхідність у `useRef`-обхідних рішеннях для застарілих замикань і робить код ефектів чистішим та передбачуванішим. Другий надає нативний механізм збереження стану прихованих компонентів, що раніше вимагало сторонніх бібліотек або складних архітектурних рішень. Обидва API знаходяться в експериментальній фазі, але їхній вплив на патерни розробки React-додатків вже відчутний. Розробникам, які готуються до технічних співбесід у 2026 році, варто не лише розуміти теорію, а й мати практичний досвід використання цих хуків у реальних сценаріях: live-дешборди, багатовкладкові інтерфейси, системи з аналітикою та фоновий передрендеринг. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/react-next/react-19-useeffectevent-activity-new-apis-interview-questions