### What
Following an anchor link to a hash param, and then attempting to use
`history.pushState` or `history.replaceState`, would result in an MPA
navigation to the targeted URL.
### Why
In #61822, a guard was added to prevent calling `ACTION_RESTORE` with a
missing tree, to match other call-sites where we do the same. This was
to prevent the app from crashing in the case where app router internals
weren't available in the history state. The original assumption was that
this is a rare / unlikely edge case. However the above scenario is a
very probable case where this can happen, and triggering an MPA
navigation isn't ideal.
### How
This updates `ACTION_RESTORE` to be resilient to an undefined router
state tree. When this happens, we'll still trigger the restore action to
sync params, but use the existing flight router state.
Closes NEXT-2502
`experimental.missingSuspenseWithCSRBailout` should be enabled by
default to help users to disciver unwrapped suspense boundaries.
Add more notes in the error doc about deprecation and temporary
workaround to disable it.
Closes NEXT-2157
---------
Co-authored-by: JJ Kasper <jj@jjsweb.site>
## What?
This PR introduces support for manually calling `history.pushState` and `history.replaceState`.
It's currently under an experimental flag:
```js
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
windowHistorySupport: true,
},
}
module.exports = nextConfig
```
Going forward I'll refer to `history.pushState` as `replaceState` is interchangable.
When the flag is enabled you're able to call the web platform `history.pushState` in the usual way:
```js
const data = {
foo: 'bar'
}
const url = '/my-new-url?search=tim'
window.history.pushState(data, '', url)
```
Let's start by explaining what would happen without the flag:
When a new history entry is pushed outside of the Next.js router any back navigation to that history entry will cause a browser reload as it can no longer be used by Next.js as the required metadata for the router is missing. In practice this makes it so that pushState/replaceState is not feasible to be used. Any pathname / searchParams added can't be observed by `usePathname` / `useSearchParams` either.
With the flag enabled the pushState/replaceState calls are instrumented and is synced into the Next.js router. This way the Next.js router's internal metadata is preserved, making back navigations apply still, and pathname / searchParams is synced as well, making sure that you can observe it using `usePathname` and `useSearchParams`.
## How?
- Added a new experimental flag `windowHistorySupport`
- Instruments `history.pushState` and `history.replaceState`
- Triggers the same action as popstate (ACTION_RESTORE) to sync the provided url (if provided) into the Next.js router
- Copies the Next.js values kept in history.state so that they are not lost
- Calls the original pushState/replaceState
~~Something to figure out is how we handle additional pushes/replaces in Next.js as that should override the history state that was previously set.~~
Went with this after discussing with @sebmarkbage:
- When you open a page it preserves the custom history state
- This is to solve this case: when you manually `window.history.pushState` / `window.history.replaceState` and then do an mpa navigation (i.e. `<a>` or `window.location.href`) and the navigate backwards the custom history state is preserved
- When you navigate back and forward (popstate) it preserves the custom history state
- When you navigate client-side (i.e. `router.push()` / `<Link>`) the custom history state is not preserved