Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental support for history.pushState and history.replaceState #58335

Merged

Conversation

timneutkens
Copy link
Member

@timneutkens timneutkens commented Nov 11, 2023

What?

This PR introduces support for manually calling history.pushState and history.replaceState.

It's currently under an experimental flag:

/**
 * @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:

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

@timneutkens
Copy link
Member Author

Current dependencies on/for this PR:

This stack of pull requests is managed by Graphite.

@ijjk
Copy link
Member

ijjk commented Nov 11, 2023

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
buildDuration 10.6s 10.6s N/A
buildDurationCached 6s 6.1s N/A
nodeModulesSize 199 MB 199 MB ⚠️ +28.8 kB
nextStartRea..uration (ms) 418ms 419ms N/A
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
199-HASH.js gzip 29.1 kB 29.2 kB ⚠️ +138 B
3f784ff6-HASH.js gzip 53.3 kB 53.3 kB N/A
494.HASH.js gzip 180 B 181 B N/A
framework-HASH.js gzip 45.2 kB 45.2 kB
main-app-HASH.js gzip 241 B 239 B N/A
main-HASH.js gzip 31.7 kB 31.8 kB N/A
webpack-HASH.js gzip 1.7 kB 1.7 kB
Overall change 76 kB 76.1 kB ⚠️ +138 B
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
_app-HASH.js gzip 194 B 195 B N/A
_error-HASH.js gzip 182 B 181 B N/A
amp-HASH.js gzip 504 B 506 B N/A
css-HASH.js gzip 322 B 323 B N/A
dynamic-HASH.js gzip 2.5 kB 2.5 kB
edge-ssr-HASH.js gzip 253 B 255 B N/A
head-HASH.js gzip 348 B 347 B N/A
hooks-HASH.js gzip 369 B 368 B N/A
image-HASH.js gzip 4.3 kB 4.3 kB N/A
index-HASH.js gzip 256 B 256 B
link-HASH.js gzip 2.65 kB 2.65 kB N/A
routerDirect..HASH.js gzip 311 B 311 B
script-HASH.js gzip 384 B 383 B N/A
withRouter-HASH.js gzip 307 B 308 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 3.17 kB 3.17 kB
Client Build Manifests
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
_buildManifest.js gzip 486 B 484 B N/A
Overall change 0 B 0 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
index.html gzip 528 B 526 B N/A
link.html gzip 540 B 541 B N/A
withRouter.html gzip 524 B 521 B N/A
Overall change 0 B 0 B
Edge SSR bundle Size Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
edge-ssr.js gzip 92.5 kB 92.5 kB N/A
page.js gzip 145 kB 145 kB ⚠️ +114 B
Overall change 145 kB 145 kB ⚠️ +114 B
Middleware size
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
middleware-b..fest.js gzip 623 B 627 B N/A
middleware-r..fest.js gzip 150 B 151 B N/A
middleware.js gzip 24.8 kB 24.8 kB N/A
edge-runtime..pack.js gzip 1.92 kB 1.92 kB
Overall change 1.92 kB 1.92 kB
Next Runtimes
vercel/next.js canary vercel/next.js 11-11-Add_support_for_history.pushState_and_history.replaceState Change
app-page-exp...dev.js gzip 167 kB 167 kB
app-page-exp..prod.js gzip 93.2 kB 93.2 kB
app-page-tur..prod.js gzip 93.9 kB 93.9 kB
app-page-tur..prod.js gzip 88.5 kB 88.5 kB
app-page.run...dev.js gzip 137 kB 137 kB
app-page.run..prod.js gzip 87.9 kB 87.9 kB
app-route-ex...dev.js gzip 23.8 kB 23.8 kB
app-route-ex..prod.js gzip 16.4 kB 16.4 kB
app-route-tu..prod.js gzip 16.4 kB 16.4 kB
app-route-tu..prod.js gzip 16 kB 16 kB
app-route.ru...dev.js gzip 23.2 kB 23.2 kB
app-route.ru..prod.js gzip 16 kB 16 kB
pages-api-tu..prod.js gzip 9.37 kB 9.37 kB
pages-api.ru...dev.js gzip 9.64 kB 9.64 kB
pages-api.ru..prod.js gzip 9.37 kB 9.37 kB
pages-turbo...prod.js gzip 21.8 kB 21.8 kB
pages.runtim...dev.js gzip 22.5 kB 22.5 kB
pages.runtim..prod.js gzip 21.8 kB 21.8 kB
server.runti..prod.js gzip 48.8 kB 48.8 kB
Overall change 922 kB 922 kB
Diff details
Diff for page.js

Diff too large to display

Diff for edge-ssr.js

Diff too large to display

Diff for 199-HASH.js

Diff too large to display

Commit: 358a380

@ijjk
Copy link
Member

ijjk commented Nov 12, 2023

Tests Passed

@timneutkens timneutkens marked this pull request as ready for review November 13, 2023 12:18
@kodiakhq kodiakhq bot merged commit 797fecb into canary Nov 13, 2023
60 checks passed
@kodiakhq kodiakhq bot deleted the 11-11-Add_support_for_history.pushState_and_history.replaceState branch November 13, 2023 13:32
startTransition(() => {
dispatch({
type: ACTION_RESTORE,
url: new URL(url ?? window.location.href),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: This should be based on window.location.href, to allow relative/partial URLs.

One can set search params on the current page (and maintain router state) via history.replaceState(history.state, '', '?foo=bar'), but this line fails as it's not a valid URL.

suggestion: new URL(url ?? '', window.location.href)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created issue #56636 some time ago. I spotted this experimental feature in the 14.0.3 release notes and thought it could fix it. However, when I try it, I just get TypeError: Failed to construct 'URL': Invalid URL errors. It looks to be related to this review comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @franky47. I just tested against 14.0.4-canary.1 which includes #58438 and it looks like it will now resolve #56636.

const currentState = window.history.state
const __NA = currentState?.__NA
if (__NA) {
data.__NA = __NA
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timneutkens I believe data can be very plausible be null or undefined here based on the replaceHistory API.

timneutkens added a commit that referenced this pull request Nov 16, 2023
Follow-up to #58335.

Fixes
#48110 (reply in thread)

As reported in the discussion passing a origin-relative string didn't
work as `new URL` will throw an error. This ensures the `origin`
parameter is provided to `new URL`.
Added tests for the string behavior.

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Zack Tanner <[email protected]>
@dennation
Copy link

@timneutkens thanks for this feature, it's much needed! But did you forget about useParams hook?

@doiali
Copy link

doiali commented Nov 21, 2023

https://medium.com/@moh.mir36/shallow-routing-with-next-js-v13-app-directory-2d765928c340

I've been using a solution similar to the one mentioned in above article. Unfortunately it no longer works after this pr.

What I did was implementing a useURL hook that returned stateful pathname and search. The result of this hook was always up to date for any kind of navigation (<Link>, router.push, window.history.replace, ...).
In previous nextJS versions, we didn't need such a workaround, because useRouter had everything we needed.

@justrealmilk
Copy link

Would it be possible to alter this addition so that it amends whatever the set function for pushState is rather than fully replacing it.

As it stands, these changes break anything else that hooks into the history API such as Plausible analytics. Presumably, other analytics function in similar ways to unobtrusively track page change.

@timneutkens
Copy link
Member Author

timneutkens commented Nov 24, 2023

@justrealmilk I'm guessing the gap between the effect call and when we read window.history.pushState / replaceState in the module is when those override? That can be changed yeah.

It would be helpful to know the exact case because then a test can be added.

@timneutkens
Copy link
Member Author

Opened a fix here: #58861, this is based on the assumption mentioned above, would be good to get confirmation of the exact case.

@manuelseisl
Copy link

Now, there is an issue with Google Tag Manager "History change" event trigger:
#58924

@alexcarpenter
Copy link

Now, there is an issue with Google Tag Manager "History change" event trigger: #58924

Similarly looks to cause an issue with Plausible 4lejandrito/next-plausible#107

@joacub
Copy link

joacub commented Nov 30, 2023

@timneutkens thanks for this feature, it's much needed! But did you forget about useParams hook?

seems like that, usePArams is not using this

@timneutkens
Copy link
Member Author

seems like that, usePArams is not using this

I didn't forget about it, it can't be supported. Client-side you don't know what routes could match. If you want to change parameters you have to use router.push / router.replace so that the request happens that correctly resolved the url.

@joacub
Copy link

joacub commented Dec 2, 2023

seems like that, usePArams is not using this

I didn't forget about it, it can't be supported. Client-side you don't know what routes could match. If you want to change parameters you have to use router.push / router.replace so that the request happens that correctly resolved the url.

So, I have to use first push and then replace ?

@timneutkens
Copy link
Member Author

No I meant you can use either, but in that case you just don't shallow route.

@joacub
Copy link

joacub commented Dec 2, 2023

No I meant you can use either, but in that case you just don't shallow route.

The problem @timneutkens is that in the case of the server request nextjs is losing the state of all tree, is not yet possible to keep the state alive in between path changes ?

For example /es/pricing to /en/pricing it should keep the state and just hydrate the changes ….,

if you just remove the keying you are using it will just hydrate, the keying you are using lose all things shout the needing

@AmirL
Copy link

AmirL commented Dec 2, 2023

May I ask when it's available in stable NextJs 13.x?

timneutkens added a commit that referenced this pull request Dec 3, 2023
## What?

Fixes
#58335 (comment).
Waiting on a reply for the exact case but my assumption is that the
history is overwritten in the user module instead of before the other JS
loads.


<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
@sunwrobert
Copy link

Why wouldnt this be added on the actual router hook? Seems a bit weird that we have to directly use window, no?

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.