Repository navigation
Description
Link to the code that reproduces this issue
To Reproduce
This reproduction demonstrates 3 issues caused by Activity-component–driven route preservation when cacheComponents: true is enabled
-
Clone the reproduction repo linked above.
-
Install dependencies:
npm install
npx playwright install chromium -
Start the dev server:
npm run dev -
Test the following behaviors in the browser:
A. Dropdown stays open
- On the home page, open the dropdown once. In this reproduction, the dropdown’s default
close-on-select behavior is intentionally prevented because of application logic requirements. - Click any dropdown button that navigates to an Item page.
- Navigate back to the home page.
→ The dropdown is still open because the component never unmounts.
B. Dialog behavior breaks because mount-time logic never runs again
Each row in the dropdown contains two navigation buttons:
- “View Item” (eye icon) → navigates to
/item/[id]without search params - “Add Item” (plus icon) → navigates to
/item/[id]?newEntry=true
Expected behavior:
- “Add Item” → dialog should open on arrival
- “View Item” → dialog should remain closed
Actual behavior:
- Reload the home page to start clean.
- Open the dropdown and click “Add Item” (
?newEntry=true).
→ Dialog opens as expected. - Use the browser Back button to return home.
- Click “View Item” (no
newEntryparam).
→ The dialog is still open even though the URL does not include the param.
Now demonstrate that once closed, the dialog never opens again:
- Reload the home page again.
- Open the dropdown and click “Add Item” (
?newEntry=true), then close the dialog manually. - Navigate back to the home page.
- Open the dropdown and click “Add Item” again (
?newEntry=true).
→ The dialog does not open, even though the URL includes the param.
This shows that mount-time initialization (including logic based on search params) only runs
once, and the dialog’s open state becomes permanently tied to stale state.
C. E2E test fails due to duplicate DOM
With the dev server still running, execute:
npm run test:e2e
This test visits /sign-in first (which contains Email and Password fields), then clicks the
link to navigate to /sign-up (which also contains Email and Password fields).
Because Activity keeps the previous route mounted, the hidden /sign-in page remains in the
DOM when /sign-up loads, resulting in two Email fields and two Password fields existing at
the same time.
→ Playwright strict mode fails because getByLabel("Email") and getByLabel("Password")
match multiple elements, triggering strict-mode violations.
Current vs. Expected behavior
Expected Behavior:
With cacheComponents disabled, the app behaves correctly:
- Components reset normally (dropdown closes, dialog opens/closes based on initial state passed to it)
- E2E selectors match only the elements on the active page
This is the behavior the app always had before Next.js 16 — including when using the older
use cache / PPR canary features — none of which introduced any Activity component behavior.
Current Behavior:
With cacheComponents: true, Next.js preserves previous routes using Activity:
- Components do not unmount when navigating
- Hidden DOM from previous pages remains present
- UI state persists across unrelated pages (dropdown stays open)
- Dialog components do not re-run mount-time logic
- E2E tests fail because multiple matching fields are in the DOM
Provide environment information
Binaries:
Node: 22.16.0
npm: 11.4.2
Yarn: 1.22.22
pnpm: 10.13.1
Relevant Packages:
next: 16.0.5 // Latest available version is detected (16.0.5).
react: 19.2.0
react-dom: 19.2.0
Next.js Config:
output: N/AWhich area(s) are affected? (Select all that apply)
cacheComponents
Which stage(s) are affected? (Select all that apply)
next dev (local), next start (local), Vercel (Deployed), Other (Deployed)
Additional context
All issues mentioned above disappear immediately if cacheComponents is set to false,
but doing so also disables the new "use cache" and PPR features.
After upgrading to Next.js 16 and enabling cacheComponents: true, unexpected behavior began
to appear in my application. The first issue was the portal visibility bug (which I also
reported here: #85390). React fixed that bug upstream, and it was resolved in Next.js 16.0.2.
However, after upgrading, additional issues still surfaced.
Because Activity changes fundamental assumptions about route lifecycle, there is a high
chance that more unexpected behaviors will appear in applications that rely on normal
unmount-on-navigation semantics.
Each issue now requires manual workaround logic. For example, controlling the dialog open
state requires syncing it inside a useEffect because the component no longer remounts.
That workaround triggers a React ESLint warning:
Error: Calling setState synchronously within an effect can trigger cascading renders
Avoid calling setState() directly within an effect
for the E2E test to pass, the Playwright selectors must be changed to:
page.getByLabel("Email").filter({ visible: true })
This avoids strict-mode violations caused by duplicate hidden fields, even though Playwright
generally advises against relying on .filter({ visible: true }) as a primary testing strategy.
Activity
Chibuike-web commentedon Nov 29, 2025
Will this be solved by manually clearing the params on router change?
SorooshGb commentedon Nov 29, 2025
If you clear the search params, you still need a
router.refresh()orrouter.replace(), which forces an unnecessary server + client re-render. And doesn't even solve anything, the real issue is when routes change, Next.js keeps previous pages mounted via<Activity>component, so their state is preserved instead of being re-initialized when you return to them. So I can't initialize the open state of the dialog based on search params when that route is visited.In my app, it happens that I do clear the search params after closing the dialog. And I could do a little workaround, by deriving the state and passing both the derived state and the open state to the dialog like this:
And in the dialog:
This doesn’t solve the underlying state-persistence issue, but it keeps the dialog in sync with
?newItem=trueon initial page load, and then hands control back to the dialog’s ownopenstate after it’s closed. This matters because the dialog trigger lives on the same page and must still be able to open the dialog independently.SorooshGb commentedon Nov 29, 2025
Just yesterday I hit another unintended and unexpected issue.
Here’s a simplified example:
Imagine you have a
/new-productpage that displays a form for creating a new product. You submit the form, get redirected to the product page, work with the product do whatever needs to be done there. Then you’re ready to add another one, so you click the “New Product” link again.You land on
/new-productand every single field is still filled with the values from the previous product you created.Now you also have to add a
useEffectcleanup to manually reset your form fields, which is completely unnecessary in a normal routing model.The number of regressions and workarounds this introduces is ridiculous. And the worst part is that Next.js could easily change or remove the
<Activity>behavior in a future update — at which point we’d have to remember every place we implemented these hacks and extra complexity and painstakingly revert them.Chibuike-web commentedon Nov 29, 2025
Yh. I have had similar issues like the form. I solved it using RHF which provides a reset function.
I think they would have to separate the activity component feature from cache components. There is a reason this is still opt for now because they would need to iron out some issues like this.
Also the idea is great honestly. Sometimes when a user fills a form and navigate to another route mistakenly, with this feature they don't have to fill the form again unless they submit which would manually reset the form.
The best case scenario is to make the activity component feature separate and opt in too. So in your case, u can simply disable it.
I also think the best case scenario for now is to turn off cache components for now instead of implementing a work around. Like you said, they might change the behavior in next major update.
mdj-uk commentedon Nov 29, 2025
Hacky workaround if you want the old behaviour:
useEffectcleanup functions still fire when you navigate away from the current page, so use that to increment some value which is used as akeyto reset state.Update: see comment below from icyJoseph this is not recommended!
Now wrap your page content if you want its state to be reset before you next navigate to it:
Though I suspect it's probably best to either not enable cacheComponents, or to embrace the new model and deal with any problems at source. e.g. on [itemId] page treat the url query params as the source of truth for the open/close state (and update them when manually opening), and on the home page close the dialog in the
onClick/onNavigatecallback of theLinkicyJoseph commentedon Dec 1, 2025
We are taking in the feedback. That component right there is not recommended, keep its usage to a minimum, it de-opts any nested Activities too.
The thing is that, eventually, user and library code are likely to use Activity and your components wrapped by them would also need to handle this behavior. What we are seeing here is that more education will be needed on React's side, and hopefully library maintainers will implement some clean up and controls accounting for Activity's presence somewhere in the tree.
When an Activity changes to 'hidden', React keeps it in the DOM and preserves state, but it also counts as a React unmount - so you can clean up on useEffects and
ref.Once again, we are taking in the feedback, thanks for taking the time to write up and follow up.
gghdev commentedon Dec 4, 2025
I think it's a bit frustrating that the previous caching features, albeit experimental, are now bundled up with
Activity. That would be fine ifActivitywasn't a breaking change, but it is. Even patterns which I assume are common are broken by this -- for example, usinguseActionState'sstate(which can't be reset) withuseEffect. There are other approaches but, for large projects, it'll take a large amount of refactoring just to useuse cache.Alexandredc commentedon Dec 8, 2025
Same issue here and i discovered this behavior after a major refactor to use cacheComponent.
Agreed with @Chibuike-web this feature should be separated from cacheComponent.
Aralf commentedon Dec 18, 2025
I'm precisely facing an issue because of
Activitykeeping the state ofuseActionState. I am using intercept and parallel routing to show a modal, because opening the modal is on a layout for 3 different pages, I just used anuseEffectto "close" the modal upon having a success state and return to the right page back.Now because of
Activitythat success state persists; so once you do an action that is successful you are not able to open the modal again.I was not expecting this behavior as part of cache components.
jonasg339 commentedon Dec 22, 2025
Please separate Activity from cache components, make Activity opt-in on a route-level. Wrapping routes inside Activity to preserve state should TOTALLY be not the default.
Consider this example:
A data-heavy application where you create/update data objects via forms - a very typical use case actually.
Just as an example, the created data objects are displayed in a table and clickable - when you click one you visit a route where the form for editing this data object appears pre-filled with the current values of that object.
Typical for such a situation where those forms can get really complex is to rely on client state libraries like react-hook-form for example -> therefore the form values are tied to the client state, so far so good.
In the old model without Activity wrapped routes (cacheComponents disabled), the data for this route gets loaded inside a Suspense, then propagated to the client side form. This lead to fresh data when you click a data object in the table - because data got dynamically loaded inside an RSC every time, and client state got re-created on a new route visit.
In the new model WITH Activity wrapped routes, everything about this behaviour is broken. You essentially cannot use client side libraries for form state management anymore. More detailed: If i click on a data object the first time, the route is visited and the form is prefilled correctly with fresh data. If i now go back and someone else/or something server side leads to a data change in that object and then i revisit that route i still see my old data due to client state preserving. Even though the new changed data is correctly fetched inside the RSC, its not getting applied correctly since the client state is not re-initialized with the new data, therefore i see my old out-of-date client state. And there is actually no way to change this behaviour at the moment without working around this with some sketchy manual client state clearing.
I don't think this is what you wanted to achieve with cacheComponents in the first place - you wanted to deliver more control and flexibility about what is getting cached and what not, and then force us to cache ALL client state? This isn't flexible at all and basically makes cacheComponents unusable outside of the lab.
Please make this Activity client state thing opt-in on a route level
JesperCph commentedon Jan 4, 2026
When Cache Components is activated, and all routes are wrapped in
<Activity>, how should I handle an parallel route with a simple<Dialog isOpen>? When the route isn’t unmounted on router.back(), then there’s no reliable way forisOpento ever become true again, making it impossible to reopen the onparallel router agingDosen't this effectively break the recommended Next.js pattern for modals using parallel routes: https://nextjs.org/docs/app/building-your-application/routing/parallel-routes#modals
The suggested useEffect unmount workaround introduces race conditions in more complex scenarios where server actions revalidate data, since this increments the componentKey and remounts the parallel route before the action completes on the client and cleanup can occur.
Please give developers back control here. Forcing when it has such destructive side effects and unclear implications makes this extremely risky to use in real-world applications, and effectively blocks us from using Cached Components altogether.
Aralf commentedon Jan 9, 2026
This will not work if you are using
useActionStateas there is no mechanism to reset the state, so it cannot be clean up/reset on useEffects upon unmount.Fix page keeping state after navigating away.
75 remaining items
✅ Corrige les selectors E2E pour cacheComponents (2/2)
✅ Corrige les selectors E2E pour cacheComponents
✅ Corrige les selectors E2E pour cacheComponents (2/2)
✅ Corrige les selectors E2E pour cacheComponents
✅ Corrige les selectors E2E pour cacheComponents (2/2)
ftognetto commentedon Aug 10, 2026
Another data point: a CRUD app where the canonical server-rendered form is affected too
Next.js 16.3.0, React 19.2.6,
cacheComponents: true. Production app, ~50 formmount points. Sharing what we found while fixing it, because two details don't
seem to be in the thread yet.
The scenario is the plainest one there is: list → open a record → edit →
save → redirect back to the list → open the same record again. The form shows
the values from before the save. Server data is fine (the detail page is
dynamic and the RSC payload is fresh); what's stale is the client instance,
revived from the hidden
<Activity>because the bfcache is keyed by URL, notby history entry — so a forward
pushto a previously visited URL restores itjust like a back navigation would.
1. Three independent storages freeze, and one of them has no fix in user land
useActionState— the initial state is captured at mount and there is noreset API, so nothing in user land can clear it without remounting (this is
what useActionState retains state when using <Activity> — how to reset useActionState while preserving input value #90107 is asking for);
useState(props.x)— the initializer never runs again;We assumed uncontrolled inputs would recover on their own, since React updates
the
defaultValueattribute when the prop changes. They don't. A minimal test(no Activity involved, just a re-render with a new
defaultValue):The displayed value stays
"A"even when the user never touched the field— React assigns
valueat mount, which sets the DOM dirty flag, so the contentattribute stops driving what's rendered from then on.
The consequence is the reason we're commenting: this affects the canonical
Next.js form —
<form><input defaultValue={record.title} /></form>rendered bya Server Component, with zero client state. Under Activity there is currently no
shape of a server-prefilled form that recovers by itself. Either you remount, or
you call
form.reset()imperatively. That makes this less of an"app state hygiene" problem than the thread suggests.
2.
bfcacheIdhelps, but it can't be the general answerIt works for our main scenario, and we shipped it. Two limits:
reached with different filters (
/volunteers→/volunteers?org=5) revivesthe same instance with the same id: anything derived from search params at
mount stays stale, and the documented escape hatch can't reach it.
checker we had to write ourselves, 41 places where client state is seeded from
server props. Getting that right by review is not realistic — we ended up
running the checker in CI just to keep the invariant.
Also worth noting for the docs' preferred remedy ("derive a key from your
data"): it doesn't exist for creation forms. There's no record and no
updatedAt, and a constantkey="new"means the next creation revives theprevious instance — you submit, go back to the list, click "new" again, and the
form is still filled in. In our app 23 of 51 mount points handle creation.
What we'd ask for
A route-segment opt-out, as already requested in #88044 — something like
export const preserveState = falseon a page or layout. Today the onlyopt-out with the right granularity is
cacheComponents: false, which alsotakes
use cacheand PPR with it, and the segment config list in 16.3 hasnothing in between.
Separately, and maybe simpler: when a segment is recreated by a push/replace,
uncontrolled inputs inside it could be reset to their current
defaultValue.That alone would fix the canonical form without asking anyone to key anything.
Happy to share the checker or the per-category numbers if they're useful.
samselikoff commentedon Aug 10, 2026
@ftognetto You said
but that's not true. A forward navigation is a new history entry and will generate a new bfcacheId (regardless if the URL is the same). So it should address the issue with the stale input value.
(I know it's still frustrating that this is something that needs a workaround. It's totally unreasonable and we are working on a better fix for this use case.)
ftognetto commentedon Aug 11, 2026
@samselikoff You're right, sorry — I got that wrong.
The instance does get revived (the
<Activity>boundary is keyed by thesegment URL), which is why the stale value appears at all. But the CacheNode is
new, so
bfcacheIdchanges and keying on it fixes the input. That's exactlywhat we shipped, so my description contradicted our own working code.
One case where it doesn't help: search-param-only navigations keep the same id
by design, so state seeded from the URL at mount can't be reset that way.
The part I'd still flag: React doesn't update a mounted input when
defaultValuechanges — not even when the user never touched the field. So aplain Server Component form (
<input defaultValue={record.title} />, no clientstate at all) shows stale values too, until something remounts it or calls
form.reset(). There's nothing for the app to key there.Good to hear a better fix is coming. If it helps, we're happy to run a canary
against our app — a production CRUD with ~50 server-prefilled forms — and
report back.
Fix stale redirect marker in hidden Activity causing hard navigation (v…
Fix stale redirect marker in hidden Activity causing hard navigation (v…
Fix stale redirect marker in hidden Activity causing hard navigation (v…
Fix stale redirect marker in hidden Activity causing hard navigation (v…
lesha1201 commentedon Sep 12, 2026
Having it restoring a client state for back/forward navigation makes a total sense since this is what browsers were doing for a long time. But I think restoring the client state for a new forward history entry does more harm than good for both developers and users.
Users might make an assumption that if some state were preserved (e.g. in an input) then it's implemented in the app and they can go navigating to other pages without any hesitation that their state will be lost. But in reality, it only lives for N amount of route navigations and that amount might be changed in the future. And it might cause unexpected behavior for the users and frustrate them.
For developers, it requires to handle a lot of cases manually and sometimes it might hard to handle all the cases. Even on Vercel, I see a lot of small issues after they've started using Cache Components.
I don't see how it might handle many cases out-of-box when it only lives a certain unconfigurable amount of route navigations. Most of the time, developers still will have to implement the logic that preserves state manually and having that default behavior with Activity only makes it more difficult because now it requires to track all client components and make sure that they cleanup the state if needed when they're used in Activity. It's quite sad that people can't use cache components without that default behavior for client components.
docs: use visible route locators with Cache Components (#101346)