Глибокий розбір React useEffectEvent: stale closures, підписки, listener-и, таймери й аналітика в React 19.2
Практичний deep dive по React useEffectEvent для React 19.2: як виправляти stale closures, зменшувати reconnect churn у підписках, тримати listener-и й таймери актуальними та будувати чистіші Effects для аналітики без ref-хаків і приглушення dependency-правил.

Гайд з React 2026 Primitives і переходу в еру Compiler
Цей розділ присвячений React `useEffectEvent`: виправленню stale closures, дизайну Effect, межам підписок, патернам для listeners і таймерів, custom Hooks та callbacks аналітики, які лишаються актуальними без reconnect churn.
Усі статті в цьому гайді
01
Огляд: React 2026 primitives і мислення епохи compiler
Велика архітектурна картина: що змінилося і де кожен primitive справді доречний.
02
useActionState deep dive: mutation flows, optimistic UI та integration patterns
Коли useActionState прибирає boilerplate, а коли data layer все ще лишається власником задачі.
03
React <Activity />: зберігайте state, ставте Effects на паузу і рендерьте у фоні
Реальні патерни, компроміси продуктивності та пастки для tabs, drawers і shell UI.
04
useEffectEvent deep dive: дизайн Effects, підписки та аналітика
Linter-friendly межі Effect без stale closures і зайвого reconnect churn.
що ви отримаєте
Цей розділ — про дизайн Effect, а не про trivia навколо Hook. Питання не в тому, «де я можу використати useEffectEvent?», а в тому, «які частини цього Effect справді реактивні, а які — просто реакція на подію, якій потрібні свіжі значення?»
Stale-closure bug — це симптом, який усі швидко помічають: callback усередині Effect читає застарілі значення. Але глибша проблема — архітектурна. Команди часто складають в один Effect дві різні роботи.
Перша робота — синхронізація: підключитися до чат-кімнати, повісити listener, запустити таймер, підписатися на SDK або ініціалізувати browser API. Друга — логіка реакції: показати notification з актуальною темою, підставити свіжий analytics metadata, перевірити mute flag чи відформатувати дані під поточну locale. Ці частини старіють по-різному й не завжди мають реагувати на той самий набір залежностей. [1][2][5]
До useEffectEvent команди часто розв’язували цю напругу або надто широкими dependency arrays, що створювали reconnect churn, або mutable refs, які вирішували stale reads, але робили дизайн складнішим для code review. useEffectEvent існує саме для того, щоб зробити цей поділ явним. [1][2]
React 19.2 представляє useEffectEvent як одну з ключових можливостей релізу. [2]
Тут усе ще важлива базова рекомендація React: Effects потрібні для синхронізації із зовнішніми системами, а не для кожної зміни state чи user action. [5]
Скріншот секції why-this-hook-existsЯкщо команда запам’ятає лише один блок із цієї статті, нехай це буде саме він.
Знайдіть зовнішню систему
Effect має синхронізуватися з чимось поза React: підпискою, таймером, DOM listener, browser API чи SDK. Якщо зовнішньої системи немає, документація React каже, що вам, ймовірно, Effect узагалі не потрібен. [5]
Виносьте event-подібну логіку реакції в useEffectEvent
Простий тест
Питайте себе: «Якщо це значення зміниться, я маю перепідключитися або перевісити listener?» Якщо так — лишайте його залежністю. Якщо ні, але callback усе ще потребує найсвіжіше значення в момент події, тут і з’являється сенс useEffectEvent. [1]
Офіційні пояснення React постійно повертаються до прикладів із chat room, бо саме вони найшвидше показують проблему. Connection має бути реактивним до roomId. Notification усе одно має бачити актуальну theme або інший UI state. Це дві різні відповідальності.
Щойно ви моделюєте їх окремо, межу effect стає простіше рев’ювати: Effect відповідає за setup і teardown connection, а Effect Event — за те, що робити, коли ця connection генерує подію. [1][2]
import { useEffect, useEffectEvent } from "react";
type Props = {
roomId: string;
theme: "light" | "dark";
};
declare function createConnection(roomId: string): {
on(event: "connected", cb: () => void): void;
connect(): void;
disconnect(): void;
};
declare function showNotification(message: string, theme: Props["theme"]): void;
export function ChatRoom({ roomId, theme }: Props) {
const onConnected = useEffectEvent(() => {
showNotification("Connected!", theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on("connected", () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return null;
}Це найчистіший “aha moment” для цього API: найсвіжіше значення і реактивна dependency — не завжди одна й та сама річ. [1][2]
Саме в підписках цей патерн починає давати цінність майже миттєво. Chat, live metrics, billing streams, Firebase-подібні listeners, media APIs і сторонні SDK мають однаковий збійний патерн: setup повинен визначатися невеликим набором значень, тоді як callback часто хоче ширший набір “latest” UI values.
Типовий smell — dependency array, що росте кожен спринт: roomId, serverUrl, theme, muted, locale, trackingContext, featureFlags, plan, segment. У цей момент Effect уже не описує external synchronization boundary — він просто описує все, що випадково згадали всередині вкладеного callback.
React також показує, що useEffectEvent особливо добре працює всередині reusable hooks. Custom hook може обгорнути переданий callback у useEffectEvent, і тоді сам hook лишається підписаним лише на реальні connection inputs, не примушуючи reconnect на кожен parent rerender з новою callback identity. [1][4]
import { useEffect, useEffectEvent } from "react";
type Message = { id: string; body: string };
type Options = {
serverUrl: string;
roomId: string;
muted: boolean;
locale: string;
onReceiveMessage: (message: Message) => void;
};
declare function createConnection(options: {
serverUrl: string;
roomId: string;
}): {
connect(): void;
disconnect(): void;
on(event: "message", cb: (message: Message) => void): void;
};
declare function playSound(name: "message"): void;
declare function formatMessagePreview(message: Message, locale: string): Message;
export function useChatRoom({
serverUrl,
roomId,
muted,
locale,
onReceiveMessage,
}: Options): void {
const onMessage = useEffectEvent((message: Message) => {
const nextMessage = formatMessagePreview(message, locale);
if (!muted) {
playSound("message");
}
onReceiveMessage(nextMessage);
});
useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
connection.on("message", (message) => {
onMessage(message);
});
return () => connection.disconnect();
}, [serverUrl, roomId]);
}Гайд React по custom Hooks показує один із найкорисніших production-кейсів: тримайте підписку прив’язаною до реальних connection inputs, а не до callback identity. [4]
Скріншот секції subscriptionsСаме в коді аналітики команди найчастіше хочуть дві суперечливі речі одночасно: стабільні підписки та свіжий context.
Уявіть listener відеоплеєра, billing event stream, observer callback або websocket event. Сама підписка має залежати від identity ресурсу: відео, акаунта, observer target або channel. Але tracking payload, який вона генерує, має містити актуальні plan, experiment bucket, locale, route metadata або назву product surface. Це поточний контекст, а не identity підключення.
Саме тут useEffectEvent читати простіше, ніж патерн на кшталт ref.current = latestStuff. Ref теж може спрацювати, але він приховує намір. Effect Event говорить прямо: «цей callback є частиною історії Effect, але він не є synchronization dependency». [1][2]
import { useEffect, useEffectEvent } from "react";
type Props = {
accountId: string;
plan: "free" | "pro" | "team";
experiment: string;
routeName: string;
};
declare function subscribeToBillingEvents(
accountId: string,
onEvent: (eventName: string) => void,
): () => void;
declare function track(event: string, props: Record<string, string>): void;
export function BillingAnalytics({
accountId,
plan,
experiment,
routeName,
}: Props) {
const onBillingEvent = useEffectEvent((eventName: string) => {
track("billing_event", {
eventName,
plan,
experiment,
routeName,
});
});
useEffect(() => {
return subscribeToBillingEvents(accountId, (eventName) => {
onBillingEvent(eventName);
});
}, [accountId]);
return null;
}Цей патерн особливо гарний у product analytics, бо чітко відділяє «до чого ми зараз підключені?» від «якими саме даними ми маємо підписати цю подію в цю секунду?».
Таймери, listeners, observers і SDK callbacks ставлять те саме питання: які значення мають керувати setup/cleanup, а які потрібно лише читати в момент події? [1]
| Comparison point | Залишити реактивним у Effect | Перенести в useEffectEvent |
|---|---|---|
| Interval | Запуск і очищення таймера, а також значення на кшталт enabled або delay, які мають його перевстановлювати | Те, що відбувається на кожному tick, коли ця логіка потребує найсвіжіших props або state |
| Window listener | Навішування та зняття listener, а також реальні event target і event type | Handler-логіку, якій потрібні актуальні filters, labels, locale або mute flags |
| Observer callback | Створення і disconnect observer, а також його target та observer options | Найсвіжіші analytics metadata або UI reaction у момент спрацювання callback |
| Third-party SDK subscription | Ініціалізацію та teardown SDK, а також resource identity, що визначає підписку | Актуальний UI context або formatting event, який використовується в момент emit від SDK |
Практичне читання
Нехай wiring належить Effect. Нехай відповідь на подію, яка має лишатися актуальною без розширення synchronization boundary, належить useEffectEvent. [1]
У документації є кілька деталей, які легко пропустити, але в більших кодбейсах вони справді важливі.
Останній пункт варто підкреслити окремо, бо useEffectEvent дуже легко почати застосовувати надмірно. Він покращує дизайн Effect, але не виправдовує існування Effect, який узагалі не мав бути написаний.
офіційно підтримуються
React прямо підтримує використання useEffectEvent усередині власних custom Hooks. Для reusable APIs навколо subscriptions і listeners це велика справа. [1][4]
тримайте локально
Effect Events мають викликатися лише з Effects або інших Effect Events у тому самому компоненті чи hook. Не передавайте їх далі як звичайний callback prop. [1]
custom effect hooks
React hooks plugin документує shared settings additionalEffectHooks для custom effect hooks, щоб команди могли послідовно лінтити wrapper hooks. [3][7]
часто це найкраще рішення
Рекомендація React «You Might Not Need an Effect» нікуди не зникла. Іноді правильний рефакторинг — не useEffectEvent, а повне винесення логіки з непотрібного Effect. [5]
Reference-сторінка чітко фіксує межі: Effect Events локальні для Effects, навмисно виключені з dependency arrays і не є general-purpose callback primitive. [1]
Скріншот секції interesting-detailsСам Hook маленький. Більшість помилок виникає тоді, коли його використовують як лазівку, а не як інструмент дизайну.
Використовувати useEffectEvent, щоб приховати значення, яке насправді має перезапускати Effect. Якщо зміна значення повинна повторно підписувати, перевішувати listener або перепідключати — це все ще dependency. [1]
Ставитися до Effect Events як до звичайних event handlers або передавати їх через props. У reference React прямо зазначає: їх слід викликати з Effects або інших Effect Events, а не з render-логіки чи довільних component boundaries. [1]
Лишати гігантські Effects і просто переносити випадкові рядки в Effect Event, поки linter не замовкне. Це не рефакторинг, а dependency laundering.
Пропускати питання «чи взагалі потрібен тут Effect?». Документація React досі радить видаляти непотрібні Effects замість того, щоб прикрашати їх новими API. [5]
Найпростіша перевірка здорового глузду
Якщо після рефакторингу ви не можете одним реченням пояснити synchronization boundary, значить Effect, найімовірніше, і далі робить надто багато.
Ці варіанти розв’язують різні проблеми. Якщо ставитися до них як до взаємозамінних, effect-код швидко стає брудним.
Використати useEffectEvent
Лишити ширші dependencies
Правильний вибір, коли змінене значення справді змінює synchronization boundary. Reconnect — не баг, якщо саме підключення реально змінилося. [1]
Використати ref
Це й далі валідний escape hatch для окремих сценаріїв із mutability, але для event-подібної callback-логіки він зазвичай менш самоочевидний, ніж Effect Event. Обирайте його, коли проблема саме в mutability, а не в effect event design. [1]
Видалити Effect
Найкраще рішення, коли зовнішньої системи немає, а логіка має жити у render-коді, derived state або звичайному event handler. Часто це і найчистіший, і найшвидший фікс. [5]
Використовуйте це під час cleanup-проходу на React 19.2 або при review custom Hooks, що обгортають subscriptions і listeners.
Проаудітьте кожен Effect, який торкається зовнішньої системи.
Почніть із sockets, listeners, timers, observers, media APIs і SDK wrappers. Це найцінніші кандидати для перегляду. [5]
Запишіть реальну synchronization boundary.
Назвіть значення, які справді мають спричиняти повторний setup і cleanup.
Використовуйте useEffectEvent лише для event-подібної callback-логіки.
Якщо логіка не запускається з Effect або іншого Effect Event, їй, ймовірно, місце деінде. [1]
Видаляйте Effects, що не синхронізуються із зовнішньою системою.
Не перетворюйте непотрібні Effects на “хитрі” Effects. Просто прибирайте їх. [5]
Тестуйте і listener churn, і freshness callback-ів.
Мета — і менше зайвих teardown-ів, і правильна робота з latest values у момент, коли подія реально відбулася.
Очікуваний результат
Після хорошого rollout Effects стають простішими для пояснення, простішими для linting і рідше перепідключаються через нерелевантний presentational state.
Він дозволяє event-подібній логіці, що запускається з Effect, читати найсвіжіші props і state без повторної синхронізації всього навколишнього Effect. Це особливо корисно для subscriptions, listeners, timers і analytics callbacks. [1][2]
Ні. Значення, які справді визначають synchronization boundary, мають і далі лишатися в dependency array. `useEffectEvent` корисний тоді, коли callback-логіка потребує найсвіжіших значень, але ці значення не повинні перезапускати subscription чи listener. [1]
Не завжди. Ref усе ще має валідні escape-hatch сценарії. Але для event-подібної логіки, що запускається з Effects, `useEffectEvent` зазвичай краще передає намір і краще вкладається в офіційну effect-модель React. [1][2]
Так. React прямо документує цей патерн, і це один із найкорисніших production-кейсів, бо він дозволяє reusable hooks тримати subscriptions стабільними й водночас викликати найсвіжіший callback споживача. [1][4]
Ні. Effect Events мають викликатися з Effects або інших Effect Events у тому самому компоненті чи hook, а не передаватися далі як звичайні callback props. [1]
Використовувати `useEffectEvent` як лазівку для dependencies замість того, щоб запитати себе, що саме визначає synchronization boundary. Друга типова помилка — не видалити непотрібні Effects на самому початку. [1][5]
Так, за правильної конфігурації. React hooks plugin документує shared settings `additionalEffectHooks`, щоб custom effect hooks можна було лінтити послідовно. [3][7]
Ми включаємо лише джерела, які напряму підтверджують поради, caveats і приклади з цього розділу.
Чимало болю під час React-оновлень — це не синтаксис. Це дизайн-борг, захований усередині Effects: змішані відповідальності, шумні dependencies, reconnect churn і аналітика, приклеєна не в той шар.
PAS7 Studio може допомогти проаудити ці межі, визначити, які Effects треба видалити, а які — переробити, і перетворити перехід на React 19.2 на міграційний план із реальними інженерними guardrails.
useEffectEvent deep dive: дизайн Effects, підписки та аналітика