next.js
56d95137 - bfcacheId: Opt out of state preservation (#93633)

Commit
64 days ago
bfcacheId: Opt out of state preservation (#93633) When `cacheComponents` is enabled, the App Router preserves state across navigations by rendering inactive routes inside React `<Activity>` boundaries. As a default behavior, this is a huge convenience because it lets you navigate between routes without resetting or losing ephemeral UI state (scroll position, expand/collapse, in-progress form edits). Previously the only way to do this was to explicitly track each state with an external state manager, or hoist it to a parent component. It's still the often the case that you should be explicitly tracking all important UI state, anyway, so that it survives a hard refresh of the app, or the browser window being accidentally closed. For example, forms draft states should be persisted to a server-side database or local storage engine. If you're already doing that, then it doesn't matter so much whether client state is preserved via `<Activity>` boundaries or not. For the long tail of ephemeral state that is not tracked, Next.js's philosophy is that it's a better UX default to preserve as much emphemeral state as possible. It's easier to model the cases where you _do_ want state to be reset on navigation as exceptions, compared to the other way around. However, this is a significant change compared to the pre-Cache Components previous behavior of Next.js, and compared to other web frameworks, and indeed the browser's own native bfcache (which implements state restoration for history traversal navigations only, not push/replace). There's are also lots of existing codebases that may rely on the current behavior, and may break subtly under these new semantics. Even if this new default unlocks better UX patterns, we don't want to force everyone to migrate all their code all at once. So, this PR introduces a drop-in mechanism for opting out of state preservation when navigating to a previously visited route. The API is exposed as `useRouter().bfcacheId`. It's intended to be passed to a React `key`: <form key={useRouter().bfcacheId}> The id is contextual: read from a layout, you get the layout's id; read from a page, you get the page's id. It's stable across back/forward navigations, `router.refresh()`, server actions that call `refresh()`, and search-param- or hash-only navigations — i.e., any time the surrounding segment is preserved. It changes when the segment is freshly created by a push or replace into a different route. An important detail is that the previous id is restored during a back/ foward navigation. So state preservation will still work if you navigate via the browser's back button. Why add this to `useRouter()` instead of giving it its own hook? The intent is communicate that `bfcacheId` is not considered an idiomatic pattern — the recommended fix for "I want this state to reset on navigation" is almost always something else: an explicit reset in a submit handler, or a key derived from the underlying data (e.g., a draft id from the server). `useRouter` is the hook where we expose low-level APIs that are supported but are only recommended for advanced or exceptional cases. (For example, instead of `router.push()`, you should almost always use a `<Link>` component instead.)
Author
Parents
Loading