The Good Tech Companies - React Activity: When A Render No Longer Guarantees an Effect
Episode Date: September 11, 2026This story was originally published on HackerNoon at: https://hackernoon.com/react-activity-when-a-render-no-longer-guarantees-an-effect. Discover how React 19.2 Activit...y changes component lifecycles, Effects, state, and cleanup—and how to avoid hidden bugs in production. Check more stories related to undefined at: https://hackernoon.com/c/undefined. You can also check exclusive content about #react, #react-19, #react-hook, #react-development, #frontend, #react-components, #useeffect, #good-company, and more. This story was written by: @socialdiscoverygroup. Learn more about this writer by checking @socialdiscoverygroup's about page, and for more stories, please visit hackernoon.com. React 19.2’s lets you hide UI while preserving its state and DOM—but it also changes the lifecycle assumptions your code can safely make. Rendering doesn’t guarantee an Effect. Hidden Activity subtrees can render without their Effects mounting. Never create side effects during render. Subscriptions and other resources should be created and cleaned up within the same Effect, or use useSyncExternalStore for external stores. StrictMode is your early warning system. Its extra render and Effect cycles can expose impure rendering and asymmetric cleanup before they become production bugs. Think carefully about resource ownership. If a WebSocket, timer, or listener should survive hidden UI, manage it above the Activity boundary. The DOM stays mounted. Effects can be cleaned up while DOM nodes remain, so media and imperative widgets may need explicit cleanup. State preservation isn't always desirable. Activity works well for tabs and editors, but forms may unexpectedly retain old values and validation state. Hidden doesn't mean frozen. Activity subtrees can still render in the background and consume memory and resources. Activity can enable pre-rendering, but not every loading strategy preloads data. Effects won't run while hidden; Suspense-compatible loading can start during render. The key takeaway: Before using , don't just ask whether it preserves state—ask what code in the subtree relies on the old render → Effect → cleanup lifecycle sequence.
Transcript
Discussion (0)
This audio is presented by Hacker Noon, where anyone can learn anything about any technology.
React activity.
When a render no longer guarantees an effect by Social Discovery Group.
React 19.2 introduced as a powerful way to hide UI while preserving its state and DOM.
But beneath this seemingly simple feature is an important life cycle shift.
A component can render without its effects ever mounting.
That difference can expose hidden bugs in legacy code from subscriptions created during render
to cleanup logic tied too closely to component visibility. In this article, we explore what React
Activity changes, why strict mode matters, and what to check before making part of your application.
React 19. 2 introduced less than activity greater than an API that lets you hide UI while preserving
its state and DOM. Turns into, we recently replaced a conditional render with activity in one of our
projects. A little later we noticed that something was leaking. At first I suspected activity, because that
the obvious recent change. It wasn't the cause. The actual problem was buried in an old hook that
subscribed to an external store during render. The hook had effectively been relying on one
assumption for years, if the component renders, an effect will eventually run and clean everything up.
Activity was simply the first thing that made that assumption fail in production. How activity
actually works. The most useful mental model for me ended up looking like this. Visible state
is preserved DOM as visible component renders as usual effects are mounted hidden
state is preserved DOM stays mounted, but as hidden component can still render. E. G. Props
changes effects are not mounted when activity transitions from visible to hidden. React hides its
contents using display. None cleans up its effects, but preserves both the state and the DOM. Hidden
children can still render when their props change, just at a lower priority. When activity
becomes visible again, React restores the UI with its previous state and recreates the effects.
If an activity starts out hidden, its effects simply don't mount. This was the part one initially got
wrong. I was still thinking about the component life cycle roughly like this. Render down pointing
arrow effect down pointing arrow cleanup but with activity, that sequence is not something you can rely on.
A hidden activity may render and simply stay hidden. Render down pointing arrow activity stays hidden down
pointing arrow no effect that difference sounds small, but it is enough to expose code that creates
resources during render and expects an effect to clean them up later. The first production bug,
a side effect during render. Our project had an old hook that had been around for several years.
Let's call it use Legacy Store. It had been written a long time ago, was used all over the
code base, had test coverage, and nobody really looked inside it anymore. For new code,
it was simply an existing abstraction that people trusted.
For the sake of the example, imagine it looked like this.
From the outside, the new component looks completely harmless.
The developer working on second tab may have no idea that somewhere inside the hook
there is a manual subscription to an external store.
Before activity, this code could appear to work perfectly well for years.
The second tab would only mount when the user actually opened it.
That resulted in a familiar sequence.
2nd tab mounts second tab unmounts the hook was already incorrect store subscribe as a side effect
and side effects should not happen during render you cannot assume that every render will result in a
commit followed by an effect but this particular bug could remain invisible for a long time then we
wanted to preserve the state of the second tab so we replaced conditional rendering with activity
once we switched to activity the hook's old life cycle assumption stopped holding react can render second
tab ahead of time, which means use legacy store, can call store. Subscribe before the tab ever
becomes visible. Under the activity contract described above, that render does not have to result
in the effect being mounted. The subscription already exists, while the cleanup that is supposed to
call unsubscribe may never exist at all. Activity did not create an invalid life cycle. It simply
made a previously ignored scenario possible. A resource is created during render even though ITS clean
UP depends on a future effect. The worst part is that the problem is not in the code that introduced
activity. The bug lives several abstraction layers deeper, inside a hook written years AGOAND used
throughout the project. Greater than so what should the hook look like? The minimum fix is to make
the subscription set up and clean up belong to the same effect. Now set up and clean up belong to the same
effect. While the activity is hidden, there is no subscription. When the UI becomes visible,
the effect creates one, and when it is hidden again, the effect cleans it up. If this is actually an
external store, there is usually no reason to implement the subscription manually with use effect in the
first place. React provides use sync external store specifically for this use case. For example, if
store, subscribe, returns an object with an unsubscribe method. Now the contract is explicit. React gets a
subscription function, a store snapshot, and a cleanup function. That explicit
It's ownership is exactly what the old hook was missing.
Responsibility for the subscription had been split between render and an effect.
Use sync external store removes the invalid assumption itself instead of merely fixing
the specific way it surfaced with activity.
Why strict mode should have exposed this earlier?
Looking at the bug afterwards, there was an uncomfortable realization.
Strict mode had been trying to tell us about this for a long time.
From the component's point of view, nothing looks suspicious.
The bad part is hidden inside use.
Legacy Store. In development, strict mode intentionally performs two useful checks. It invokes render
functions an extra time to detect impure rendering, and it runs an additional set-up right-pointing
arrow-set-up cycle for effects to uncover cleanup issues. These checks do not run in production.
For our hook, a simplified initial render can be thought of like this. Render number one right-pointing
arrow use Legacy Store. Right-pointing arrow subscribe a right-pointing arrow render result
discarded render number two right pointing arrow use legacy store right pointing arrow subscribe b right
pointing arrow commit the problem is already obvious subscribe a exists even though the first render was
discarded there is nobody responsible for unsubscribing it the extra effect check makes the asymmetry
even more visible effect set up down pointing arrow strict mode clean up down pointing arrow unsubscribe
be down pointing arrow effect set up again but the effect set up in our legacy hook does not call
subscribed at all. The subscription is created during render. So the whole design is asymmetric.
Render creates the resource, while the effect only attempts to destroy it. A correct hook behaves
very differently. Now the additional strict mode check is perfectly symmetrical. Effect set up right
pointing arrow subscribe clean UP right pointing arrow unsubscribe effect set up right pointing arrow
subscribe. That is why this case changed how I think about strict mode. If a legacy hook starts
creating duplicate subscriptions, firing extra requests, or behaving strangely under the additional
effect life cycle, that is not just some annoying development-only behavior. Strict mode is genuinely
exposing code that depends too heavily on one particular render right-pointing arrow effect
right-pointing arrow cleanup sequence. With activity, the exact same flaw can turn into a real
production bug. In fact, the activity documentation itself recommends strict mode as a way to catch
these problems early. If strict mode is not enabled at the root of your application yet,
enable it. There is one important nuance here. If strict mode is only enabled for a subtree,
React does not run the additional initial effect cycle for that subtree. Doing so would create
an effect ordering that could not happen in production without the corresponding parent life cycle.
For the most complete coverage, it is better to enable strict mode at the root. Effects are no longer
the same thing as a component life cycle. Fixing side effects during render,
is only half of the story. We ran into another case where the effect itself was completely fine,
then the component gets wrapped in an activity, and suddenly the WebSocket disconnects every time
the panel is hidden. That is expected behavior given the activity lifecycle, but it exposes a different
problem. The life cycle of a background process has been coupled to the life cycle of a specific
U.I component. If the WebSocket should exist only while the user can see notifications panel,
then keeping the effect inside the activity is exactly right.
But if the connection should remain active even while the panel is hidden,
ownership of that connection needs to move above the activity boundary.
The simplest option is to move the hook up.
Now use notifications connection lives in app,
which means hiding notifications panel no longer tears down the WebSocket connection.
If the data is needed in multiple parts of the application,
the same ownership model can be expressed through a provider.
The panel itself simply consumes the already
available data from context, and the activity sits below the provider. Now the life cycle looks like
this. Notifications provider right pointing arrow use notifications connection. Right
pointing arrow web socket connected notifications panel visible right pointing arrow reads data from
context notification panel hidden right pointing arrow panel effects are cleaned up right
pointing arrow web socket keeps running notifications panel visible again right pointing arrow receives
the latest data from context in other words. Activity forces us to define a
explicitly who actually owns the life cycle of an external resource. If the WebSocket belongs
specifically to that panel, keeping the effect inside the panel is fine. If the connection should
live independently of whether the panel is currently visible, its life cycle needs to be managed
above the activity boundary. A useful question to ask is simple. Should this process exist only
while the user can see this particular UI, the DOM stays around even after effect cleanup?
One detail surprised me more than it probably should have.
Cleaning up the effects does not mean the DOM is gone.
From the effects point of view, hiding an activity feels a bit like an unmount.
From the DOM's point of view, it definitely isn't. React hides the subtree with display.
None, but keeps the nodes around. That means this. Has very different behavior from this.
With the second version, the less than video greater than node disappears. With activity, it doesn't,
In the official React example, the video continues playing even after the activity becomes hidden, https colon slash slash react.
Dev, reference, react, activity, UTM underscore source equals Chad GPT.
Comhash My Dash Hidden Dash Components Dash have dash unwanted dash side dash affects the same category of issue applies to less than audio greater than, less than iFrame greater than, and imperative third party widgets that rely on the DOM node being removed.
The solution is to explicitly connect cleanup to the activity lifecycle.
I am using use layout effect here because the cleanup is directly related to visually hiding the element.
The React documentation notes that a regular use effect may be delayed in this kind of scenario,
for example because of suspense or a view transition.
The DOM node itself is still preserved.
If the user returns to the tab, the less than video greater than can retain browser managed state such as the current playback position.
So activity lets us preserve the DOM and its existence.
associated browser state, but we no longer get cleanup for free through DOM removal.
State is preserved, which is sometimes good and sometimes not.
State preservation is one of the main reasons to use activity.
It can also be a problem.
Suppose a Create Dialogue previously looked like this.
The user opens the form, enters some data, closes it, and then opens it again.
Because Create User form was unmounted, the new instance starts with clean state.
Now we change the implementation.
The behavior changes. Close right-pointing arrow U.I. Hidden Open right-pointing arrow previous state restored the form may retain entered values, validation errors, a local draft, selected options, scroll position, and even uncontrolled DOM state. For tabs, this is often exactly what we want. For forms, it often is not. That is why activity should not be used mechanically as a replacement for every conditional render. If closing the U.I semantically means, this instance is finished.
A normal unmount may be the correct behavior.
If you still want to preserve the DOM or the rest of the subtree while resetting the state of a particular form instance, you can explicitly change its key.
Each new opening changes the key.
So React creates a fresh create user form with fresh state even though the activity itself remains alive.
It is important to remember that with this approach, the entire form tree IS fully unmounted and then mounted again.
A hidden subtree can still render.
At one point I also caught myself thinking.
of a hidden activity as something close to a frozen page. It isn't. If data changes, very expensive
screen can still render. React gives hidden work a lower priority, but the subtree is still alive.
This matters once you start keeping several large screens around. Navigation may feel faster,
but you are trading that for retained DOM, memory, and some amount of background work. If we keep
10 expensive pages alive, we get faster navigation back to previously opened pages and preserved state,
but we pay for it with memory usage, retained DOM, and potential background renders.
I would use activity where preserving state provides a real user-facing benefit.
An editor with an unsaved draft is a good example, especially if it integrates a heavyweight
text editor or can contain a large document that the user should not los. On the other hand,
a screen whose state does not need to survive and which these are rarely revisits can continue
using conditional rendering, activity and preloading. Activity can prepare hidden Ui
ahead of time. For example, React may render a tab before the user opens it. There is an important
caveat, though not every data loading strategy will start loading during this kind of pre-render. If the
request is triggered inside use effect, then the hidden render itself will not trigger the request.
This follows directly from the activity lifecycle described earlier. Code inside the effect
will not run until the subtree becomes visible in the effect IS created. So this version of fetch
posts does not give us true preloading. With suspense, however, things can work differently. If
data loading is part of render and uses a suspense-compatible mechanism, the request can start
during the pre-render. For example, using use, the screen itself can remain inside a hidden
activity and be wrapped in suspense. Now the sequence is different. Activity hidden down-pointing
arrow React pre-renders posts down-pointing arrow posts calls use, posts resource. Get down-pointing arrow data
as not ready right pointing arrow loading starts down pointing arrow promise suspends the render
through suspense the request starts not because activity somehow runs the effect early. It starts because
data loading is part of render and React can pre-render a hidden activity. When the user later opens
the tab, the data may already be available. Activity right pointing arrow visible down pointing
arrow posts renders again down pointing arrow data as already loaded down pointing arrow screen
appears without waiting for a new request posts resource is intentionally abstract here.
In a real application, this code be a framework or library that supports suspense and knows how to
cache promises. The implementation of the cache is not the point of this example. The important distinction
is, use effect right pointing arrow loading starts only after the effect mounts
suspense plus use right pointing arrow loading can start during render so if you add a hidden
activity hoping to preload the next screen, but the data still does not start loading and
until the user opens it, the first thing to check E is where the request is actually initiated.
Unexpected E2E consequences. There is another practical issue that shows up less in application
code and more in E2E tests. Before activity, only one email input exists in the DOM. After activity,
the DOM nodes for both tabs can now remain mounted. One of the inputs is hidden, but it
still exists. Existing playwright codes such as can now fail in strict mode because the locator matches
multiple elements. You can explicitly account for visibility. Playwright supports this kind of filtering,
although it generally recommends using a more robust way to uniquely identify the target element
whenever possible. An even better approach is to locate the element within the active UI container.
Now the test expresses what the user is actually doing, interacting not with any email field
somewhere in the DOM, but with the email field inside the currently visible tab. What I check before
reaching for activity. I don't treat activity as a replacement for conditional rendering anymore.
Before using it, I usually look for a few things. One, anything happening during render that
shouldn't be there? Subscriptions are the obvious example, but not the only one. Two, what should
keep running when this UI disappears? If a socket, timer, listener, or other process should outlive
the screen, it probably shouldn't be owned by an effect inside that activity. Three, is some cleanup
currently happening only because the DOM node gets removed? Video, audio, iframe, and imperative
widgets are worth checking. Four, do I actually want the old state when the user comes back?
For a tab, probably. For a, create new item, dialogue, maybe not. And finally, how expensive is this
subtree to keep around? Activity can make returning to a screen much nicer, but preserving a screen is not
free. Hidden UI can still occupy memory, retain DOM and render in the
background. The part one would keep in mind, before using activity, I mostly thought about it as a
way to preserve state. After debugging this issue, I think the life cycle difference is more important.
It is easy to implicitly rely on this sequence. Render right pointing arrow mount right
pointing arrow effect right pointing arrow unmount right pointing arrow cleanup but that is not React's
contract. Render, effects, DOM, and component state have related but distinct life cycles,
and activity makes that separation particularly visible. React has been moving in this direction
for a long time. Strict mode is specifically designed to find impure renders and asymmetric effects,
while concurrent rendering in general requires render to stay free of external side effects.
In our case, the subscription bug turned out to be a good example.
Activity did not break the cleanup. It simply made a scenario possible that the old hook
had never accounted for. So the main question I now ask before using activity is no longer
longer. It is. Written by Sergey Levkovich, senior front-end developer at Social Discovery Group.
Thank you for listening to this Hackernoon story, read by artificial intelligence.
Visit hackernoon.com to read, write, learn and publish.
