Skip to content
TrackPodcasts
technologySep 11, 202619:00

React Activity: When A Render No Longer Guarantees an Effect

About this episode

This 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 Activity 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.

Get every episode summarized

Each time The Good Tech Companies publishes, we email you a written briefing from the transcript — the topics, who appeared, and any specific claims, with the ad reads skipped.

Email me new episodes

Free for 3 shows. No card needed.

Hosts & guests

Transcript ready

209 searchable segments. Every word is indexed and playable.

React Activity: When A Render No Longer Guarantees an Effect

The Good Tech Companies

0:00
19:00

Full transcript

The Good Tech CompaniesReact Activity: When A Render No Longer Guarantees an Effect. Machine-transcribed; use the interactive transcript above to jump the player to any line.

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. Too introduced as a powerful way to hide UI while preserving its state and DOM. But beneath this seemingly simple feature is an important lifecycle 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 clean up 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. Too 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 was 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 as 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. Second tab mounts second tab unmounts the hook was already incorrect. Store, subscribe, is 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 ridden years a GOA and D used throughout the project. Greater than so what should the hook look like? The minimum fix is to make the subscription setup and cleanup belong to the same effect. Now setup and cleanup 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 stores 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 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 components 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 setup right pointing arrow cleanup, right pointing arrow setup 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 setup down pointing arrow strict mode cleanup down pointing arrow unsubscribed b down pointing arrow effect setup again but the effect setup in our legacy hook does not call subscribe 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 setup right pointing arrow subscribe clean UP right pointing arrow unsubscribe effect setup 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 lifecycle 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 lifecycle. 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 lifecycle. 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 lifecycle of a background process has been coupled to the lifecycle of a specific UI 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 an 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 lifecycle looks like this. Notifications provider right pointing arrow use notifications connection. Right pointing arrow websocket connected notifications panel visible right pointing arrow reads data from context notifications panel hidden right pointing arrow panel effects are cleaned up right pointing arrow

websocket keeps running notifications panel visible again right pointing arrow receives the latest data from context in other words. Activity forces us to define explicitly who actually owns the lifecycle 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 lifecycle 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 chat gpt. Com hash my dash hidden dash components dash have dash unwanted dash side dash effects. 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 is 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

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 UI 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 UI 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 I S 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 lose. On the other hand, a screen whose state does not need to survive and which the serrarily 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 and 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-render's posts down pointing arrow posts calls use, posts resource. Get, down pointing arrow data is 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 is already loaded down pointing arrow screen appears without waiting for a new request post 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 pre-load the next screen. But the data still does not start loading until the user opens it. The first thing to check 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 code 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 out live 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, dialog, 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 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. It is. Written by Sergei Levkovich, Senior Frontend 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.

More episodes

More from The Good Tech Companies

View all episodes →