Skip to content

[pull] canary from vercel:canary#1041

Merged
pull[bot] merged 5 commits into
code:canaryfrom
vercel:canary
May 12, 2026
Merged

[pull] canary from vercel:canary#1041
pull[bot] merged 5 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented May 12, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

timneutkens and others added 5 commits May 11, 2026 21:31
## What?

Follow-up to #93247. There was a failure on the React 18 tests. Missed
in #93247 as they only run on `canary`.
#93287)

### What?

Extends the card-based instant error overlay (#92638) to metadata,
viewport, and sync IO errors, and updates the matching build/CLI
messages to a consistent structured format.

### Why?

After the blocking-route redesign, metadata, viewport, and sync IO
errors still used the old prose format with no visual fix guidance.

### Demo

- Metadata:
[runtime](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/12-cookies-in-metadata)
·
[dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/13-fetch-in-metadata)
- Viewport:
[runtime](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/14-cookies-in-viewport)
·
[dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/15-fetch-in-viewport)
- Sync IO:
[Math.random()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/38-math-random-no-instant)
·
[Date.now()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/39-date-now-no-instant)
·
[crypto.randomUUID()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/40-crypto-random-no-instant)
- Sync IO in Client:
[Math.random()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/44-client-math-random-no-suspense)
·
[Date.now()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/43-client-date-no-suspense)
·
[crypto.randomUUID()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/45-client-crypto-no-suspense)
- [Fix overview (all
cards)](https://error-messages-overhaul-ibsl.labs.vercel.dev/fix-overview)

### How?

**Overlay**

- New `dynamic-metadata`, `dynamic-viewport`, and `sync-io` error types
in `errors.tsx`, all rendered through `InstantRuntimeError` with
kind-specific fix cards
- `isSyncIOError()` detects sync IO via the docs URL pattern, mirroring
`isRuntimeVariant()`

**Build & CLI messages**

- Headlines aligned to "Next.js encountered..." across all error
families
- `blocking-route-messages.ts`: metadata/viewport errors restructured
from prose to "Ways to fix this:" bullets
- Sync IO messages extracted into `sync-io-messages.ts`, mirroring
`blocking-route-messages.ts`. Helpers renamed to `createSyncIOError` /
`createSyncIORuntimeError` / `createSyncIOClientError` to match the
`create*Error` pattern
- [x] fix tests
- [ ] add experimental flag and only enable in adapter? or is this cheap
enough to just always do?
- [x] disable for webpack




Closes PACK-6541

For nextjs/adapter-vercel#24
Newer version of #89534

- Add a hashes mapping to the NFT file
- Forward that information to the adapter via `assetsHashes` for
functions


Basically zero-cost:
```
commit ad728b1 (HEAD -> mischnic/server-paths-manifest-2, origin/mischnic/server-paths-manifest-2)
pnpm next build   351.22s user, 51.55s system, 811% cpu, 49.654 total
pnpm next build   354.52s user, 53.39s system, 837% cpu, 48.706 total
pnpm next build   344.02s user, 63.62s system, 749% cpu, 54.368 total
pnpm next build   353.74s user, 54.25s system, 833% cpu, 48.970 total

commit 04294cb (origin/canary, origin/HEAD)
pnpm next build   344.24s user, 53.88s system, 798% cpu, 49.845 total
pnpm next build   340.89s user, 52.40s system, 784% cpu, 50.138 total
pnpm next build   347.26s user, 51.91s system, 802% cpu, 49.741 total
pnpm next build   347.04s user, 52.65s system, 803% cpu, 49.764 total
```
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.)
@pull pull Bot locked and limited conversation to collaborators May 12, 2026
@pull pull Bot added the ⤵️ pull label May 12, 2026
@pull pull Bot merged commit 56d9513 into code:canary May 12, 2026
5 of 8 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants