All Articles
CategoryFull-Stack
Reading Time
13 min read
Published
2026-08-05
Word Count
3,049words

Grab a coffee — this one is a deep dive!

React 19.2: Activity, useEffectEvent, and cacheSignal

Summary

A look at React 19.2's Activity, useEffectEvent, and cacheSignal APIs with real code examples — what problems they solve and how to migrate.

  • Activity hides children with display:none in hidden mode, cleans up their Effects, but preserves state — switching back to visible restores state and DOM.
  • useEffectEvent separates event-like logic inside an Effect from reactive logic; the function it returns sees the latest values but doesn't go in the dependency array.
  • cacheSignal returns an AbortSignal during render, aborted when the render finishes or is cancelled; outside render it returns null.
  • Before using useEffectEvent, upgrade eslint-plugin-react-hooks to @latest (per the react.dev 19.2 announcement).
React 19.2: Activity, useEffectEvent, and cacheSignal

When React 19.2 landed on npm on October 1, 2025, three APIs arrived at once: <Activity>, useEffectEvent, and cacheSignal. In the React team's own words, this was the third release in a year — the third link in the chain after React 19 in December and React 19.1 that followed. In this post you'll see the React 19.2 Activity API, the useEffectEvent hook, and the cacheSignal mechanism through real code examples: what problem each one solves and how to migrate a project to them today.

💡 Pro Tip: Upgrade eslint-plugin-react-hooks to @latest (as of August 5, 2026, latest is 7.1.1) — otherwise the linter will try to add Effect Event functions to the dependency array.

Table of Contents

What's in the 19.2 Line, and Which Version Is Current

React 19.2 was announced on react.dev on October 1, 2025, and published to npm the same day. The React team describes this release as the third in a year: React 19 in December 2024, then React 19.1, then React 19.2 in October 2025 (the announcement text says June for 19.1, while the npm registry and react.dev/versions show 19.1.0 as March 28, 2025). As of this article's writing date (August 5, 2026), the 19.2 line is still under active maintenance; according to npm registry records, the latest patch, 19.2.8, was published on July 21, 2026.

Version
Released
Note
React 19.0
December 2024
Main 19 release
React 19.1
March 2025
Interim release
React 19.2.0
October 1, 2025
Activity, useEffectEvent, cacheSignal introduced
React 19.2.8
July 21, 2026
Latest 19.2 patch (npm registry time data)

What stands out here: react.dev's own "Releases" page doesn't list 19.2.8 as a separate row — it stops adding patch rows after 19.2.7. Seeing the real patch date means checking the npm registry's time field instead, since react.dev's changelog may not be current at the patch level — when verifying which version runs in production, cross-check with npm view react versions or a registry query rather than trusting react.dev alone.

The common thread across 19.2's three main APIs is this: each one opens up an area you'd describe as "React was already doing this, but it wasn't giving you control." Activity exposes a hide/show behavior that already existed internally in React as a component; useEffectEvent uses a formal API to separate two kinds of logic that should already have been separated inside Effects (reactive and event-like); and cacheSignal shows that server rendering already had a lifecycle, and that this lifecycle is now observable. None of the three is a "new capability" — each makes an existing behavior predictable and testable.

Activity: The Hide-But-Preserve-State Pattern

<Activity> is a new component that lets you split your app into controllable, prioritizable parts. It has two modes: visible and hidden. If the mode prop isn't given, it defaults to visible.

Switching to hidden mode visually hides the children (display: "none") and cleans up their Effects, but keeps state. Switching back to visible restores state, shows the children, and re-creates their Effects. Hidden children still re-render on new props — just at lower priority than visible content.

tsx
1import { Activity, useState } from "react";
2 
3function App() {
4 const [tab, setTab] = useState<"feed" | "profile">("feed");
5 
6 return (
7 <>
8 <Activity mode={tab === "feed" ? "visible" : "hidden"}>
9 <FeedPage />
10 </Activity>
11 <Activity mode={tab === "profile" ? "visible" : "hidden"}>
12 <ProfilePage />
13 </Activity>
14 </>
15 );
16}

In this example, when tab becomes "profile", FeedPage isn't removed from the DOM — it's only hidden, so scroll position, form state, and internal component state are all preserved. If you'd used conditional rendering ({isVisible && <FeedPage />}), FeedPage would mount from scratch every time.

Mode
Visibility
Effects
State
visible
no hiding, normal render
mounted, runs
preserved
hidden
display: none
unmounted (cleaned up)
preserved, updated at lower priority

In practice, the most typical use case is a structure like the tab bar in mobile apps. When a user switches from the "Feed" tab to "Profile," classic conditional rendering fully unmounts FeedPage; when the user comes back, scroll position resets, open filters disappear, and the network request fires again. With Activity, this tab switches to hidden mode but stays in the DOM — when you return, everything picks up right where it left off. The cost is that hidden tabs keep occupying memory, so it makes sense to use Activity not everywhere, but in flows where UX genuinely suffers from state loss (multi-step forms, tabbed dashboards, search results).

useEffectEvent and the Dependency Array Hell

A classic problem: inside useEffect, both "reactive" logic (like establishing a connection) and "event-like" logic (like showing a notification) live together. If the latter depends on a value (say, theme), the entire Effect re-runs unnecessarily whenever that value changes.

tsx
1// BEFORE: the chat room reconnects unnecessarily when theme changes
2function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
3 useEffect(() => {
4 const connection = createConnection(roomId);
5 connection.on("connected", () => {
6 showNotification("Connected!", theme);
7 });
8 connection.connect();
9 return () => connection.disconnect();
10 }, [roomId, theme]); // also reconnects when theme changes
11}

useEffectEvent separates this "event" part from the Effect. Effect Events always see the latest prop/state values from render, but they don't go in the dependency array — because their identity is intentionally different on every render; this is a deliberate design decision.

tsx
1// AFTER: theme is no longer a dependency, but it still sees the latest value
2import { useEffectEvent, useEffect } from "react";
3 
4function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
5 const onConnected = useEffectEvent(() => {
6 showNotification("Connected!", theme);
7 });
8 
9 useEffect(() => {
10 const connection = createConnection(roomId);
11 connection.on("connected", () => onConnected());
12 connection.connect();
13 return () => connection.disconnect();
14 }, [roomId]); // theme is no longer here
15}

Two rules matter: useEffectEvent can only be called at the top level of a component or Hook (not inside a loop or condition), and the function it returns can only be called from Effects or other Effect Events.

The same pattern also shows up often in analytics/telemetry code. You don't want a page-view event to re-fire just because the user ID or session info changed, while the page itself stayed the same — but you do want it to use the latest user info at the moment it fires:

tsx
1import { useEffectEvent, useEffect } from "react";
2 
3function ProductPage({
4 productId,
5 userId,
6}: {
7 productId: string;
8 userId: string;
9}) {
10 const logPageView = useEffectEvent((id: string) => {
11 analytics.track("product_view", { productId: id, userId });
12 });
13 
14 useEffect(() => {
15 logPageView(productId);
16 }, [productId]); // userId is not a dependency, but the event always sees the latest userId
17}

Here, the page-view event doesn't re-fire when userId changes — because that would be logically wrong (the user didn't view the page again, only their session changed). But whenever logPageView is called, it always uses the latest userId value. Without useEffectEvent, this distinction would lead either to a warning suppressed with eslint-disable or to unnecessary re-triggering. Note: don't use useEffectEvent to hide "unnecessary" dependencies — react.dev considers that an anti-pattern.

cacheSignal and Server-Side Cancellation

cacheSignal returns an AbortSignal when called during render; if called outside of render, it returns null. This signal lets you track the lifetime of an operation wrapped with cache() (for example, a fetch or a DB query): the signal is aborted when React finishes rendering, cancels it, or hits an error. cacheSignal is currently only usable in Server Components; calling it inside a Client Component always returns null.

tsx
1import { cache, cacheSignal } from "react";
2 
3const dedupedFetch = cache(fetch);
4 
5async function Component({ url }: { url: string }) {
6 const response = await dedupedFetch(url, {
7 signal: cacheSignal() ?? undefined,
8 });
9 const data = await response.json();
10 return <DataView data={data} />;
11}

In practice, this prevents in-flight fetch requests from completing pointlessly when server-side rendering ends early (for example, when the render is aborted or a parent Suspense boundary throws an error) — once the signal is aborted, the fetch can be aborted too.

This gains particular value in Server Component trees with many parallel data sources. If a dashboard page has multiple cache()-wrapped queries running in parallel, without cacheSignal those queries could keep completing in the background even though the render was aborted. Tying cacheSignal() to every request lets you collapse React's own render lifecycle into a single cancellation point — you don't need to manage a separate AbortController, set up a timeout, or manually track component unmounting.

What This Means Alongside Next.js 16.3

cacheSignal gains particular value in environments where server rendering has become cancellable, and Next.js 16.3 is a release focused on exactly that direction. Two feature sections stand out in its release notes (plus a separate section for experimental features). Under "Improvements for today's apps," "Fewer prefetch requests" has links bundle smaller payloads, triggering fewer prefetch calls. "Instant Navigations" is a separate, opt-in toolkit; under it sit "Instant Insights" (a devtool that surfaces slow navigations) and "Partial Prefetching" (fine-grained control over how much content a link prefetches from the target page). These changes aren't directly tied to cacheSignal — it's a separate release note — but both point the same way: server rendering and prefetching can now be controlled in more detail and terminated early when needed. A Server Component using cacheSignal() inside cache() can now stop unnecessary work automatically when its render is cut short, regardless of the framework layer's prefetch strategy.

Migration: What Code Should Change Today

Two steps are needed before moving to useEffectEvent:

  • Upgrade the lint package: pull eslint-plugin-react-hooks to @latest — otherwise the linter will try to add Effect Events as a dependency.
  • Separate the "event" logic: move "event-like" code inside Effects, such as notifications/logging/analytics, into useEffectEvent; leave only the genuinely reactive part (connection, subscription) in the Effect.
bash
1npm install eslint-plugin-react-hooks@latest --save-dev

On the Activity side, replacing existing conditional render blocks ({isVisible && <Page />}) with <Activity mode="visible|hidden"> makes sense especially in tab structures and multi-step flows where form state needs to be preserved. For cacheSignal, adding signal: cacheSignal() ?? undefined to existing cache()-wrapped fetch calls is a one-line change.

The step most often skipped during migration is the rule that useEffectEvent "can only be called from inside an Effect." Call an Effect Event directly during render and React throws; pass it to a click handler instead and the eslint-plugin-react-hooks linter flags it — a deliberate restriction, since Effect Events can only be called locally, can't be passed to other components or added to a dependency array, and a stable identity for them would serve no purpose and could even mask bugs. Before a large useEffectEvent sweep, target the Effects with the most eslint-disable comments first — those lines already signal a dependency array problem.

Relationship with React Compiler

The first stable release of React Compiler (v1.0) shipped a week after React 19.2, on October 7, 2025. The Compiler focuses on automatically preventing unnecessary re-renders; Activity, useEffectEvent, and cacheSignal solve different problems (visibility/state preservation, Effect dependency management, server cancellation). The two aren't mutually exclusive — in a project with the Compiler enabled, all three of these APIs keep working exactly the same way, because while the Compiler deals with render optimization, these three sit at a different layer (lifecycle, event separation, cancellation).

In practice: once you turn on the Compiler, you can stop hand-writing most useMemo/useCallback wrappers, but that doesn't remove the need for useEffectEvent — the Compiler optimizes rendering, not when and why an Effect should re-run. It doesn't change Activity's state-preservation behavior either; the two operate independently, so you can migrate in whichever order you like.

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

We've put together a concrete checklist you can follow while migrating the three APIs in this article to your own project. Check off each item one by one to complete the migration step by step.

FAQ

What does the Activity component do in React?

<Activity mode={visible|hidden}> lets you manage a piece of UI with a "keep it in the background" mechanism instead of conditional rendering. In hidden mode, children are hidden with display: none, their Effects are cleaned up, but state is preserved; switching back to visible restores the state and DOM, and re-creates the Effects.

What useEffect problem does useEffectEvent solve?

If "event-like" logic inside an Effect depended on a value, the entire Effect would re-run unnecessarily whenever that value changed (for example, a chat room reconnecting when the theme changes). useEffectEvent separates this part from the Effect; the function it returns sees the latest values but doesn't go in the dependency array.

What does cacheSignal do in Server Components?

It returns an AbortSignal when called during render; this signal is aborted when React finishes rendering, cancels it, or hits an error. cache()-wrapped fetch/DB calls can use this signal to cancel work that's no longer needed. Called outside of render, it returns null.

What should I watch out for when moving to React 19.2?

For useEffectEvent you need to upgrade eslint-plugin-react-hooks to @latest; otherwise the linter can produce incorrect warnings. Effect Events can only be defined at the component/Hook top level and can only be called from inside Effects — the linter verifies this too.

What's the difference between Activity and conditional rendering?

Conditional rendering ({isVisible && <Page/>}) fully unmounts and remounts the component, resetting state. Activity, in hidden mode, keeps the component in the DOM (hidden), preserves its state, and only cleans up its Effects — when you return, state picks up right from where it left off.

Update (September 2026)

After this article was published, the React 19.2 line handed off to a newer release: React 19.3.0 was announced on react.dev on September 9, 2026, and npm's latest dist-tag for react now points to 19.3.0. As of this article's publish date (August 5, 2026), "the 19.2 line is still under active maintenance" and "the latest patch is 19.2.8" were accurate; today, the current line is React 19.3. The Activity, useEffectEvent, and cacheSignal APIs covered here remain usable the same way in 19.3 — none of the three was removed. The changelog includes fixes for <Activity> and useEffectEvent: "Fix useSyncExternalStore missing store mutations that happened while an <Activity> tree was hidden" (#36947), "Don't let errors escape a hidden <Activity>" (#35074), and "Fix useEffectEvent to read the latest values in forwardRef and memo components" (#34831); <Activity> is now also supported in Flight (Server Components) (#34697). Source: react.dev/blog/2026/09/09/react-19-3.

Conclusion

React 19.2's three APIs solve independent but complementary problems: Activity handles the balance between visibility and state preservation, useEffectEvent the reactive/event split inside Effects, and cacheSignal the cancellation lifecycle of server rendering. For similar concurrency and state-management principles on the Swift side, see Swift Observation Framework: Modern State Management with @Observable or Swift Structured Concurrency Deep Dive. Following the React ecosystem on the cross-platform side, React Native New Architecture 2026: Fabric, TurboModules, and Bridgeless Mode is a good complement. For the Swift-side counterpart of this dependency/effect discipline, see Async/Await Best Practices: Swift Concurrency Mastery; for the Android-side counterpart of render optimization and state-preserving patterns, see Jetpack Compose 1.7 Performance: Strong Skipping + Stability.

Sources

Tags

#React 19.2#Activity#useEffectEvent#cacheSignal#React Compiler#Server Components#frontend
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share