@tanstack/history 1.15.13 → 1.20.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -2,23 +2,45 @@
2
2
  // This implementation attempts to be more lightweight by
3
3
  // making assumptions about the way TanStack Router works
4
4
 
5
+ export interface NavigateOptions {
6
+ ignoreBlocker?: boolean
7
+ }
8
+
9
+ type SubscriberHistoryAction =
10
+ | {
11
+ type: Exclude<HistoryAction, 'GO'>
12
+ }
13
+ | {
14
+ type: 'GO'
15
+ index: number
16
+ }
17
+
18
+ type SubscriberArgs = {
19
+ location: HistoryLocation
20
+ action: SubscriberHistoryAction
21
+ }
22
+
5
23
  export interface RouterHistory {
6
24
  location: HistoryLocation
7
- subscribe: (cb: () => void) => () => void
8
- push: (path: string, state?: any) => void
9
- replace: (path: string, state?: any) => void
10
- go: (index: number) => void
11
- back: () => void
12
- forward: () => void
25
+ length: number
26
+ subscribers: Set<(opts: SubscriberArgs) => void>
27
+ subscribe: (cb: (opts: SubscriberArgs) => void) => () => void
28
+ push: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
29
+ replace: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
30
+ go: (index: number, navigateOpts?: NavigateOptions) => void
31
+ back: (navigateOpts?: NavigateOptions) => void
32
+ forward: (navigateOpts?: NavigateOptions) => void
33
+ canGoBack: () => boolean
13
34
  createHref: (href: string) => string
14
- block: (blocker: BlockerFn) => () => void
35
+ block: (blocker: NavigationBlocker) => () => void
15
36
  flush: () => void
16
37
  destroy: () => void
17
- notify: () => void
38
+ notify: (action: SubscriberHistoryAction) => void
39
+ _ignoreSubscribers?: boolean
18
40
  }
19
41
 
20
42
  export interface HistoryLocation extends ParsedPath {
21
- state: HistoryState
43
+ state: ParsedHistoryState
22
44
  }
23
45
 
24
46
  export interface ParsedPath {
@@ -28,59 +50,105 @@ export interface ParsedPath {
28
50
  hash: string
29
51
  }
30
52
 
31
- export interface HistoryState {
53
+ export interface HistoryState {}
54
+
55
+ export type ParsedHistoryState = HistoryState & {
32
56
  key?: string
57
+ __TSR_index: number
33
58
  }
34
59
 
35
60
  type ShouldAllowNavigation = any
36
61
 
37
- export type BlockerFn = () =>
38
- | Promise<ShouldAllowNavigation>
39
- | ShouldAllowNavigation
40
-
41
- const pushStateEvent = 'pushstate'
42
- const popStateEvent = 'popstate'
43
- const beforeUnloadEvent = 'beforeunload'
62
+ export type HistoryAction = 'PUSH' | 'REPLACE' | 'FORWARD' | 'BACK' | 'GO'
44
63
 
45
- const beforeUnloadListener = (event: Event) => {
46
- event.preventDefault()
47
- // @ts-ignore
48
- return (event.returnValue = '')
64
+ export type BlockerFnArgs = {
65
+ currentLocation: HistoryLocation
66
+ nextLocation: HistoryLocation
67
+ action: HistoryAction
49
68
  }
50
69
 
51
- const stopBlocking = () => {
52
- removeEventListener(beforeUnloadEvent, beforeUnloadListener, {
53
- capture: true,
54
- })
70
+ export type BlockerFn = (
71
+ args: BlockerFnArgs,
72
+ ) => Promise<ShouldAllowNavigation> | ShouldAllowNavigation
73
+
74
+ export type NavigationBlocker = {
75
+ blockerFn: BlockerFn
76
+ enableBeforeUnload?: (() => boolean) | boolean
55
77
  }
56
78
 
79
+ type TryNavigateArgs = {
80
+ task: () => void
81
+ type: 'PUSH' | 'REPLACE' | 'BACK' | 'FORWARD' | 'GO'
82
+ navigateOpts?: NavigateOptions
83
+ } & (
84
+ | {
85
+ type: 'PUSH' | 'REPLACE'
86
+ path: string
87
+ state: any
88
+ }
89
+ | {
90
+ type: 'BACK' | 'FORWARD' | 'GO'
91
+ }
92
+ )
93
+
94
+ const stateIndexKey = '__TSR_index'
95
+ const popStateEvent = 'popstate'
96
+ const beforeUnloadEvent = 'beforeunload'
97
+
57
98
  export function createHistory(opts: {
58
99
  getLocation: () => HistoryLocation
100
+ getLength: () => number
59
101
  pushState: (path: string, state: any) => void
60
102
  replaceState: (path: string, state: any) => void
61
103
  go: (n: number) => void
62
- back: () => void
63
- forward: () => void
104
+ back: (ignoreBlocker: boolean) => void
105
+ forward: (ignoreBlocker: boolean) => void
64
106
  createHref: (path: string) => string
65
107
  flush?: () => void
66
108
  destroy?: () => void
67
- onBlocked?: (onUpdate: () => void) => void
109
+ onBlocked?: () => void
110
+ getBlockers?: () => Array<NavigationBlocker>
111
+ setBlockers?: (blockers: Array<NavigationBlocker>) => void
112
+ // Avoid notifying on forward/back/go, used for browser history as we already get notified by the popstate event
113
+ notifyOnIndexChange?: boolean
68
114
  }): RouterHistory {
69
115
  let location = opts.getLocation()
70
- let subscribers = new Set<() => void>()
71
- let blockers: BlockerFn[] = []
116
+ const subscribers = new Set<(opts: SubscriberArgs) => void>()
72
117
 
73
- const onUpdate = () => {
118
+ const notify = (action: SubscriberHistoryAction) => {
74
119
  location = opts.getLocation()
75
- subscribers.forEach((subscriber) => subscriber())
120
+ subscribers.forEach((subscriber) => subscriber({ location, action }))
76
121
  }
77
122
 
78
- const tryNavigation = async (task: () => void) => {
79
- if (typeof document !== 'undefined' && blockers.length) {
80
- for (let blocker of blockers) {
81
- const allowed = await blocker()
82
- if (!allowed) {
83
- opts.onBlocked?.(onUpdate)
123
+ const handleIndexChange = (action: SubscriberHistoryAction) => {
124
+ if (opts.notifyOnIndexChange ?? true) notify(action)
125
+ else location = opts.getLocation()
126
+ }
127
+
128
+ const tryNavigation = async ({
129
+ task,
130
+ navigateOpts,
131
+ ...actionInfo
132
+ }: TryNavigateArgs) => {
133
+ const ignoreBlocker = navigateOpts?.ignoreBlocker ?? false
134
+ if (ignoreBlocker) {
135
+ task()
136
+ return
137
+ }
138
+
139
+ const blockers = opts.getBlockers?.() ?? []
140
+ const isPushOrReplace =
141
+ actionInfo.type === 'PUSH' || actionInfo.type === 'REPLACE'
142
+ if (typeof document !== 'undefined' && blockers.length && isPushOrReplace) {
143
+ for (const blocker of blockers) {
144
+ const nextLocation = parseHref(actionInfo.path, actionInfo.state)
145
+ const isBlocked = await blocker.blockerFn({
146
+ currentLocation: location,
147
+ nextLocation,
148
+ action: actionInfo.type,
149
+ })
150
+ if (isBlocked) {
151
+ opts.onBlocked?.()
84
152
  return
85
153
  }
86
154
  }
@@ -93,74 +161,102 @@ export function createHistory(opts: {
93
161
  get location() {
94
162
  return location
95
163
  },
96
- subscribe: (cb: () => void) => {
164
+ get length() {
165
+ return opts.getLength()
166
+ },
167
+ subscribers,
168
+ subscribe: (cb: (opts: SubscriberArgs) => void) => {
97
169
  subscribers.add(cb)
98
170
 
99
171
  return () => {
100
172
  subscribers.delete(cb)
101
173
  }
102
174
  },
103
- push: (path: string, state: any) => {
104
- state = assignKey(state)
105
- tryNavigation(() => {
106
- opts.pushState(path, state)
107
- onUpdate()
175
+ push: (path, state, navigateOpts) => {
176
+ const currentIndex = location.state[stateIndexKey]
177
+ state = assignKeyAndIndex(currentIndex + 1, state)
178
+ tryNavigation({
179
+ task: () => {
180
+ opts.pushState(path, state)
181
+ notify({ type: 'PUSH' })
182
+ },
183
+ navigateOpts,
184
+ type: 'PUSH',
185
+ path,
186
+ state,
108
187
  })
109
188
  },
110
- replace: (path: string, state: any) => {
111
- state = assignKey(state)
112
- tryNavigation(() => {
113
- opts.replaceState(path, state)
114
- onUpdate()
189
+ replace: (path, state, navigateOpts) => {
190
+ const currentIndex = location.state[stateIndexKey]
191
+ state = assignKeyAndIndex(currentIndex, state)
192
+ tryNavigation({
193
+ task: () => {
194
+ opts.replaceState(path, state)
195
+ notify({ type: 'REPLACE' })
196
+ },
197
+ navigateOpts,
198
+ type: 'REPLACE',
199
+ path,
200
+ state,
115
201
  })
116
202
  },
117
- go: (index) => {
118
- tryNavigation(() => {
119
- opts.go(index)
203
+ go: (index, navigateOpts) => {
204
+ tryNavigation({
205
+ task: () => {
206
+ opts.go(index)
207
+ handleIndexChange({ type: 'GO', index })
208
+ },
209
+ navigateOpts,
210
+ type: 'GO',
120
211
  })
121
212
  },
122
- back: () => {
123
- tryNavigation(() => {
124
- opts.back()
213
+ back: (navigateOpts) => {
214
+ tryNavigation({
215
+ task: () => {
216
+ opts.back(navigateOpts?.ignoreBlocker ?? false)
217
+ handleIndexChange({ type: 'BACK' })
218
+ },
219
+ navigateOpts,
220
+ type: 'BACK',
125
221
  })
126
222
  },
127
- forward: () => {
128
- tryNavigation(() => {
129
- opts.forward()
223
+ forward: (navigateOpts) => {
224
+ tryNavigation({
225
+ task: () => {
226
+ opts.forward(navigateOpts?.ignoreBlocker ?? false)
227
+ handleIndexChange({ type: 'FORWARD' })
228
+ },
229
+ navigateOpts,
230
+ type: 'FORWARD',
130
231
  })
131
232
  },
233
+ canGoBack: () => location.state[stateIndexKey] !== 0,
132
234
  createHref: (str) => opts.createHref(str),
133
235
  block: (blocker) => {
134
- blockers.push(blocker)
135
-
136
- if (blockers.length === 1) {
137
- addEventListener(beforeUnloadEvent, beforeUnloadListener, {
138
- capture: true,
139
- })
140
- }
236
+ if (!opts.setBlockers) return () => {}
237
+ const blockers = opts.getBlockers?.() ?? []
238
+ opts.setBlockers([...blockers, blocker])
141
239
 
142
240
  return () => {
143
- blockers = blockers.filter((b) => b !== blocker)
144
-
145
- if (!blockers.length) {
146
- stopBlocking()
147
- }
241
+ const blockers = opts.getBlockers?.() ?? []
242
+ opts.setBlockers?.(blockers.filter((b) => b !== blocker))
148
243
  }
149
244
  },
150
245
  flush: () => opts.flush?.(),
151
246
  destroy: () => opts.destroy?.(),
152
- notify: onUpdate,
247
+ notify,
153
248
  }
154
249
  }
155
250
 
156
- function assignKey(state: HistoryState) {
251
+ function assignKeyAndIndex(index: number, state: HistoryState | undefined) {
157
252
  if (!state) {
158
253
  state = {} as HistoryState
159
254
  }
160
255
  return {
161
256
  ...state,
162
257
  key: createRandomKey(),
163
- }
258
+ [stateIndexKey]: index,
259
+ } as ParsedHistoryState
164
260
  }
165
261
 
166
262
  /**
@@ -188,6 +284,14 @@ export function createBrowserHistory(opts?: {
188
284
  opts?.window ??
189
285
  (typeof document !== 'undefined' ? window : (undefined as any))
190
286
 
287
+ const originalPushState = win.history.pushState
288
+ const originalReplaceState = win.history.replaceState
289
+
290
+ let blockers: Array<NavigationBlocker> = []
291
+ const _getBlockers = () => blockers
292
+ const _setBlockers = (newBlockers: Array<NavigationBlocker>) =>
293
+ (blockers = newBlockers)
294
+
191
295
  const createHref = opts?.createHref ?? ((path) => path)
192
296
  const parseLocation =
193
297
  opts?.parseLocation ??
@@ -197,9 +301,25 @@ export function createBrowserHistory(opts?: {
197
301
  win.history.state,
198
302
  ))
199
303
 
304
+ // Ensure there is always a key to start
305
+ if (!win.history.state?.key) {
306
+ win.history.replaceState(
307
+ {
308
+ [stateIndexKey]: 0,
309
+ key: createRandomKey(),
310
+ },
311
+ '',
312
+ )
313
+ }
314
+
200
315
  let currentLocation = parseLocation()
201
316
  let rollbackLocation: HistoryLocation | undefined
202
317
 
318
+ let nextPopIsGo = false
319
+ let ignoreNextPop = false
320
+ let skipBlockerNextPop = false
321
+ let ignoreNextBeforeUnload = false
322
+
203
323
  const getLocation = () => currentLocation
204
324
 
205
325
  let next:
@@ -213,39 +333,33 @@ export function createBrowserHistory(opts?: {
213
333
  isPush: boolean
214
334
  }
215
335
 
216
- // Because we are proactively updating the location
217
- // in memory before actually updating the browser history,
218
- // we need to track when we are doing this so we don't
219
- // notify subscribers twice on the last update.
220
- let tracking = true
221
-
222
336
  // We need to track the current scheduled update to prevent
223
337
  // multiple updates from being scheduled at the same time.
224
338
  let scheduled: Promise<void> | undefined
225
339
 
226
- // This function is a wrapper to prevent any of the callback's
227
- // side effects from causing a subscriber notification
228
- const untrack = (fn: () => void) => {
229
- tracking = false
230
- fn()
231
- tracking = true
232
- }
233
-
234
340
  // This function flushes the next update to the browser history
235
341
  const flush = () => {
236
- // Do not notify subscribers about this push/replace call
237
- untrack(() => {
238
- if (!next) return
239
- win.history[next.isPush ? 'pushState' : 'replaceState'](
240
- next.state,
241
- '',
242
- next.href,
243
- )
244
- // Reset the nextIsPush flag and clear the scheduled update
245
- next = undefined
246
- scheduled = undefined
247
- rollbackLocation = undefined
248
- })
342
+ if (!next) {
343
+ return
344
+ }
345
+
346
+ // We need to ignore any updates to the subscribers while we update the browser history
347
+ history._ignoreSubscribers = true
348
+
349
+ // Update the browser history
350
+ ;(next.isPush ? win.history.pushState : win.history.replaceState)(
351
+ next.state,
352
+ '',
353
+ next.href,
354
+ )
355
+
356
+ // Stop ignoring subscriber updates
357
+ history._ignoreSubscribers = false
358
+
359
+ // Reset the nextIsPush flag and clear the scheduled update
360
+ next = undefined
361
+ scheduled = undefined
362
+ rollbackLocation = undefined
249
363
  }
250
364
 
251
365
  // This function queues up a call to update the browser history
@@ -276,52 +390,149 @@ export function createBrowserHistory(opts?: {
276
390
  }
277
391
  }
278
392
 
279
- const onPushPop = () => {
393
+ // NOTE: this function can probably be removed
394
+ const onPushPop = (type: 'PUSH' | 'REPLACE') => {
395
+ currentLocation = parseLocation()
396
+ history.notify({ type })
397
+ }
398
+
399
+ const onPushPopEvent = async () => {
400
+ if (ignoreNextPop) {
401
+ ignoreNextPop = false
402
+ return
403
+ }
404
+
405
+ const nextLocation = parseLocation()
406
+ const delta =
407
+ nextLocation.state[stateIndexKey] - currentLocation.state[stateIndexKey]
408
+ const isForward = delta === 1
409
+ const isBack = delta === -1
410
+ const isGo = (!isForward && !isBack) || nextPopIsGo
411
+ nextPopIsGo = false
412
+
413
+ const action = isGo ? 'GO' : isBack ? 'BACK' : 'FORWARD'
414
+ const notify: SubscriberHistoryAction = isGo
415
+ ? {
416
+ type: 'GO',
417
+ index: delta,
418
+ }
419
+ : {
420
+ type: isBack ? 'BACK' : 'FORWARD',
421
+ }
422
+
423
+ if (skipBlockerNextPop) {
424
+ skipBlockerNextPop = false
425
+ } else {
426
+ const blockers = _getBlockers()
427
+ if (typeof document !== 'undefined' && blockers.length) {
428
+ for (const blocker of blockers) {
429
+ const isBlocked = await blocker.blockerFn({
430
+ currentLocation,
431
+ nextLocation,
432
+ action,
433
+ })
434
+ if (isBlocked) {
435
+ ignoreNextPop = true
436
+ win.history.go(1)
437
+ history.notify(notify)
438
+ return
439
+ }
440
+ }
441
+ }
442
+ }
443
+
280
444
  currentLocation = parseLocation()
281
- history.notify()
445
+ history.notify(notify)
282
446
  }
283
447
 
284
- var originalPushState = win.history.pushState
285
- var originalReplaceState = win.history.replaceState
448
+ const onBeforeUnload = (e: BeforeUnloadEvent) => {
449
+ if (ignoreNextBeforeUnload) {
450
+ ignoreNextBeforeUnload = false
451
+ return
452
+ }
453
+
454
+ let shouldBlock = false
455
+
456
+ // If one blocker has a non-disabled beforeUnload, we should block
457
+ const blockers = _getBlockers()
458
+ if (typeof document !== 'undefined' && blockers.length) {
459
+ for (const blocker of blockers) {
460
+ const shouldHaveBeforeUnload = blocker.enableBeforeUnload ?? true
461
+ if (shouldHaveBeforeUnload === true) {
462
+ shouldBlock = true
463
+ break
464
+ }
465
+
466
+ if (
467
+ typeof shouldHaveBeforeUnload === 'function' &&
468
+ shouldHaveBeforeUnload() === true
469
+ ) {
470
+ shouldBlock = true
471
+ break
472
+ }
473
+ }
474
+ }
475
+
476
+ if (shouldBlock) {
477
+ e.preventDefault()
478
+ return (e.returnValue = '')
479
+ }
480
+ return
481
+ }
286
482
 
287
483
  const history = createHistory({
288
484
  getLocation,
485
+ getLength: () => win.history.length,
289
486
  pushState: (href, state) => queueHistoryAction('push', href, state),
290
487
  replaceState: (href, state) => queueHistoryAction('replace', href, state),
291
- back: () => win.history.back(),
292
- forward: () => win.history.forward(),
293
- go: (n) => win.history.go(n),
488
+ back: (ignoreBlocker) => {
489
+ if (ignoreBlocker) skipBlockerNextPop = true
490
+ ignoreNextBeforeUnload = true
491
+ return win.history.back()
492
+ },
493
+ forward: (ignoreBlocker) => {
494
+ if (ignoreBlocker) skipBlockerNextPop = true
495
+ ignoreNextBeforeUnload = true
496
+ win.history.forward()
497
+ },
498
+ go: (n) => {
499
+ nextPopIsGo = true
500
+ win.history.go(n)
501
+ },
294
502
  createHref: (href) => createHref(href),
295
503
  flush,
296
504
  destroy: () => {
297
505
  win.history.pushState = originalPushState
298
506
  win.history.replaceState = originalReplaceState
299
- win.removeEventListener(pushStateEvent, onPushPop)
300
- win.removeEventListener(popStateEvent, onPushPop)
507
+ win.removeEventListener(beforeUnloadEvent, onBeforeUnload, {
508
+ capture: true,
509
+ })
510
+ win.removeEventListener(popStateEvent, onPushPopEvent)
301
511
  },
302
- onBlocked: (onUpdate) => {
512
+ onBlocked: () => {
303
513
  // If a navigation is blocked, we need to rollback the location
304
514
  // that we optimistically updated in memory.
305
515
  if (rollbackLocation && currentLocation !== rollbackLocation) {
306
516
  currentLocation = rollbackLocation
307
- // Notify subscribers
308
- onUpdate()
309
517
  }
310
518
  },
519
+ getBlockers: _getBlockers,
520
+ setBlockers: _setBlockers,
521
+ notifyOnIndexChange: false,
311
522
  })
312
523
 
313
- win.addEventListener(pushStateEvent, onPushPop)
314
- win.addEventListener(popStateEvent, onPushPop)
524
+ win.addEventListener(beforeUnloadEvent, onBeforeUnload, { capture: true })
525
+ win.addEventListener(popStateEvent, onPushPopEvent)
315
526
 
316
- win.history.pushState = function () {
317
- let res = originalPushState.apply(win.history, arguments as any)
318
- if (tracking) history.notify()
527
+ win.history.pushState = function (...args: Array<any>) {
528
+ const res = originalPushState.apply(win.history, args as any)
529
+ if (!history._ignoreSubscribers) onPushPop('PUSH')
319
530
  return res
320
531
  }
321
532
 
322
- win.history.replaceState = function () {
323
- let res = originalReplaceState.apply(win.history, arguments as any)
324
- if (tracking) history.notify()
533
+ win.history.replaceState = function (...args: Array<any>) {
534
+ const res = originalReplaceState.apply(win.history, args as any)
535
+ if (!history._ignoreSubscribers) onPushPop('REPLACE')
325
536
  return res
326
537
  }
327
538
 
@@ -335,7 +546,13 @@ export function createHashHistory(opts?: { window?: any }): RouterHistory {
335
546
  return createBrowserHistory({
336
547
  window: win,
337
548
  parseLocation: () => {
338
- const hashHref = win.location.hash.split('#').slice(1).join('#') ?? '/'
549
+ const hashSplit = win.location.hash.split('#').slice(1)
550
+ const pathPart = hashSplit[0] ?? '/'
551
+ const searchPart = win.location.search
552
+ const hashEntries = hashSplit.slice(1)
553
+ const hashPart =
554
+ hashEntries.length === 0 ? '' : `#${hashEntries.join('#')}`
555
+ const hashHref = `${pathPart}${searchPart}${hashPart}`
339
556
  return parseHref(hashHref, win.history.state)
340
557
  },
341
558
  createHref: (href) =>
@@ -345,34 +562,41 @@ export function createHashHistory(opts?: { window?: any }): RouterHistory {
345
562
 
346
563
  export function createMemoryHistory(
347
564
  opts: {
348
- initialEntries: string[]
565
+ initialEntries: Array<string>
349
566
  initialIndex?: number
350
567
  } = {
351
568
  initialEntries: ['/'],
352
569
  },
353
570
  ): RouterHistory {
354
571
  const entries = opts.initialEntries
355
- let index = opts.initialIndex ?? entries.length - 1
356
- let currentState = {
357
- key: createRandomKey(),
358
- } as HistoryState
572
+ let index = opts.initialIndex
573
+ ? Math.min(Math.max(opts.initialIndex, 0), entries.length - 1)
574
+ : entries.length - 1
575
+ const states = entries.map((_entry, index) =>
576
+ assignKeyAndIndex(index, undefined),
577
+ )
359
578
 
360
- const getLocation = () => parseHref(entries[index]!, currentState)
579
+ const getLocation = () => parseHref(entries[index]!, states[index])
361
580
 
362
581
  return createHistory({
363
582
  getLocation,
364
-
583
+ getLength: () => entries.length,
365
584
  pushState: (path, state) => {
366
- currentState = state
585
+ // Removes all subsequent entries after the current index to start a new branch
586
+ if (index < entries.length - 1) {
587
+ entries.splice(index + 1)
588
+ states.splice(index + 1)
589
+ }
590
+ states.push(state)
367
591
  entries.push(path)
368
- index++
592
+ index = Math.max(entries.length - 1, 0)
369
593
  },
370
594
  replaceState: (path, state) => {
371
- currentState = state
595
+ states[index] = state
372
596
  entries[index] = path
373
597
  },
374
598
  back: () => {
375
- index--
599
+ index = Math.max(index - 1, 0)
376
600
  },
377
601
  forward: () => {
378
602
  index = Math.min(index + 1, entries.length - 1)
@@ -384,9 +608,12 @@ export function createMemoryHistory(
384
608
  })
385
609
  }
386
610
 
387
- function parseHref(href: string, state: HistoryState): HistoryLocation {
388
- let hashIndex = href.indexOf('#')
389
- let searchIndex = href.indexOf('?')
611
+ export function parseHref(
612
+ href: string,
613
+ state: ParsedHistoryState | undefined,
614
+ ): HistoryLocation {
615
+ const hashIndex = href.indexOf('#')
616
+ const searchIndex = href.indexOf('?')
390
617
 
391
618
  return {
392
619
  href,
@@ -405,7 +632,7 @@ function parseHref(href: string, state: HistoryState): HistoryLocation {
405
632
  searchIndex > -1
406
633
  ? href.slice(searchIndex, hashIndex === -1 ? undefined : hashIndex)
407
634
  : '',
408
- state: state || {},
635
+ state: state || { [stateIndexKey]: 0, key: createRandomKey() },
409
636
  }
410
637
  }
411
638