@@ -35,6 +35,7 @@ import {
35
35
PrefetchKind ,
36
36
} from './router-reducer/router-reducer-types'
37
37
import type {
38
+ PushRef ,
38
39
ReducerActions ,
39
40
RouterChangeByServerResponse ,
40
41
RouterNavigate ,
@@ -108,24 +109,44 @@ function isExternalURL(url: URL) {
108
109
return url . origin !== window . location . origin
109
110
}
110
111
111
- function HistoryUpdater ( { tree, pushRef, canonicalUrl, sync } : any ) {
112
+ function HistoryUpdater ( {
113
+ tree,
114
+ pushRef,
115
+ canonicalUrl,
116
+ sync,
117
+ } : {
118
+ tree : FlightRouterState
119
+ pushRef : PushRef
120
+ canonicalUrl : string
121
+ sync : ( ) => void
122
+ } ) {
112
123
useInsertionEffect ( ( ) => {
113
- // Identifier is shortened intentionally.
114
- // __NA is used to identify if the history entry can be handled by the app-router.
115
- // __N is used to identify if the history entry can be handled by the old router.
116
124
const historyState = {
125
+ ...( process . env . __NEXT_WINDOW_HISTORY_SUPPORT &&
126
+ pushRef . preserveCustomHistoryState
127
+ ? window . history . state
128
+ : { } ) ,
129
+ // Identifier is shortened intentionally.
130
+ // __NA is used to identify if the history entry can be handled by the app-router.
131
+ // __N is used to identify if the history entry can be handled by the old router.
117
132
__NA : true ,
118
- tree,
133
+ __PRIVATE_NEXTJS_INTERNALS_TREE : tree ,
119
134
}
120
135
if (
121
136
pushRef . pendingPush &&
137
+ // Skip pushing an additional history entry if the canonicalUrl is the same as the current url.
138
+ // This mirrors the browser behavior for normal navigation.
122
139
createHrefFromUrl ( new URL ( window . location . href ) ) !== canonicalUrl
123
140
) {
124
141
// This intentionally mutates React state, pushRef is overwritten to ensure additional push/replace calls do not trigger an additional history entry.
125
142
pushRef . pendingPush = false
126
- window . history . pushState ( historyState , '' , canonicalUrl )
143
+ if ( originalPushState ) {
144
+ originalPushState ( historyState , '' , canonicalUrl )
145
+ }
127
146
} else {
128
- window . history . replaceState ( historyState , '' , canonicalUrl )
147
+ if ( originalReplaceState ) {
148
+ originalReplaceState ( historyState , '' , canonicalUrl )
149
+ }
129
150
}
130
151
sync ( )
131
152
} , [ tree , pushRef , canonicalUrl , sync ] )
@@ -204,6 +225,28 @@ function useNavigate(dispatch: React.Dispatch<ReducerActions>): RouterNavigate {
204
225
)
205
226
}
206
227
228
+ const originalPushState =
229
+ typeof window !== 'undefined'
230
+ ? window . history . pushState . bind ( window . history )
231
+ : null
232
+ const originalReplaceState =
233
+ typeof window !== 'undefined'
234
+ ? window . history . replaceState . bind ( window . history )
235
+ : null
236
+
237
+ function copyNextJsInternalHistoryState ( data : any ) {
238
+ const currentState = window . history . state
239
+ const __NA = currentState ?. __NA
240
+ if ( __NA ) {
241
+ data . __NA = __NA
242
+ }
243
+ const __PRIVATE_NEXTJS_INTERNALS_TREE =
244
+ currentState ?. __PRIVATE_NEXTJS_INTERNALS_TREE
245
+ if ( __PRIVATE_NEXTJS_INTERNALS_TREE ) {
246
+ data . __PRIVATE_NEXTJS_INTERNALS_TREE = __PRIVATE_NEXTJS_INTERNALS_TREE
247
+ }
248
+ }
249
+
207
250
/**
208
251
* The global router that wraps the application components.
209
252
*/
@@ -371,12 +414,16 @@ function Router({
371
414
// would trigger the mpa navigation logic again from the lines below.
372
415
// This will restore the router to the initial state in the event that the app is restored from bfcache.
373
416
function handlePageShow ( event : PageTransitionEvent ) {
374
- if ( ! event . persisted || ! window . history . state ?. tree ) return
417
+ if (
418
+ ! event . persisted ||
419
+ ! window . history . state ?. __PRIVATE_NEXTJS_INTERNALS_TREE
420
+ )
421
+ return
375
422
376
423
dispatch ( {
377
424
type : ACTION_RESTORE ,
378
425
url : new URL ( window . location . href ) ,
379
- tree : window . history . state . tree ,
426
+ tree : window . history . state . __PRIVATE_NEXTJS_INTERNALS_TREE ,
380
427
} )
381
428
}
382
429
@@ -416,13 +463,66 @@ function Router({
416
463
use ( createInfinitePromise ( ) )
417
464
}
418
465
419
- /**
420
- * Handle popstate event, this is used to handle back/forward in the browser.
421
- * By default dispatches ACTION_RESTORE, however if the history entry was not pushed/replaced by app-router it will reload the page.
422
- * That case can happen when the old router injected the history entry.
423
- */
424
- const onPopState = useCallback (
425
- ( { state } : PopStateEvent ) => {
466
+ useEffect ( ( ) => {
467
+ if ( process . env . __NEXT_WINDOW_HISTORY_SUPPORT ) {
468
+ // Ensure the canonical URL in the Next.js Router is updated when the URL is changed so that `usePathname` and `useSearchParams` hold the pushed values.
469
+ const applyUrlFromHistoryPushReplace = (
470
+ url : string | URL | null | undefined
471
+ ) => {
472
+ startTransition ( ( ) => {
473
+ dispatch ( {
474
+ type : ACTION_RESTORE ,
475
+ url : new URL ( url ?? window . location . href ) ,
476
+ tree : window . history . state . __PRIVATE_NEXTJS_INTERNALS_TREE ,
477
+ } )
478
+ } )
479
+ }
480
+
481
+ if ( originalPushState ) {
482
+ /**
483
+ * Patch pushState to ensure external changes to the history are reflected in the Next.js Router.
484
+ * Ensures Next.js internal history state is copied to the new history entry.
485
+ * Ensures usePathname and useSearchParams hold the newly provided url.
486
+ */
487
+ window . history . pushState = function pushState (
488
+ data : any ,
489
+ _unused : string ,
490
+ url ?: string | URL | null
491
+ ) : void {
492
+ copyNextJsInternalHistoryState ( data )
493
+
494
+ applyUrlFromHistoryPushReplace ( url )
495
+
496
+ return originalPushState ( data , _unused , url )
497
+ }
498
+ }
499
+ if ( originalReplaceState ) {
500
+ /**
501
+ * Patch replaceState to ensure external changes to the history are reflected in the Next.js Router.
502
+ * Ensures Next.js internal history state is copied to the new history entry.
503
+ * Ensures usePathname and useSearchParams hold the newly provided url.
504
+ */
505
+ window . history . replaceState = function replaceState (
506
+ data : any ,
507
+ _unused : string ,
508
+ url ?: string | URL | null
509
+ ) : void {
510
+ copyNextJsInternalHistoryState ( data )
511
+
512
+ if ( url ) {
513
+ applyUrlFromHistoryPushReplace ( url )
514
+ }
515
+ return originalReplaceState ( data , _unused , url )
516
+ }
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Handle popstate event, this is used to handle back/forward in the browser.
522
+ * By default dispatches ACTION_RESTORE, however if the history entry was not pushed/replaced by app-router it will reload the page.
523
+ * That case can happen when the old router injected the history entry.
524
+ */
525
+ const onPopState = ( { state } : PopStateEvent ) => {
426
526
if ( ! state ) {
427
527
// TODO-APP: this case only happens when pushState/replaceState was called outside of Next.js. It should probably reload the page in this case.
428
528
return
@@ -441,20 +541,23 @@ function Router({
441
541
dispatch ( {
442
542
type : ACTION_RESTORE ,
443
543
url : new URL ( window . location . href ) ,
444
- tree : state . tree ,
544
+ tree : state . __PRIVATE_NEXTJS_INTERNALS_TREE ,
445
545
} )
446
546
} )
447
- } ,
448
- [ dispatch ]
449
- )
547
+ }
450
548
451
- // Register popstate event to call onPopstate.
452
- useEffect ( ( ) => {
549
+ // Register popstate event to call onPopstate.
453
550
window . addEventListener ( 'popstate' , onPopState )
454
551
return ( ) => {
552
+ if ( originalPushState ) {
553
+ window . history . pushState = originalPushState
554
+ }
555
+ if ( originalReplaceState ) {
556
+ window . history . replaceState = originalReplaceState
557
+ }
455
558
window . removeEventListener ( 'popstate' , onPopState )
456
559
}
457
- } , [ onPopState ] )
560
+ } , [ dispatch ] )
458
561
459
562
const { cache, tree, nextUrl, focusAndScrollRef } =
460
563
useUnwrapState ( reducerState )
0 commit comments