@tanstack/react-router 1.167.5 → 1.168.0

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.
Files changed (89) hide show
  1. package/dist/cjs/Match.cjs +118 -52
  2. package/dist/cjs/Match.cjs.map +1 -1
  3. package/dist/cjs/Matches.cjs +20 -20
  4. package/dist/cjs/Matches.cjs.map +1 -1
  5. package/dist/cjs/Scripts.cjs +36 -32
  6. package/dist/cjs/Scripts.cjs.map +1 -1
  7. package/dist/cjs/Transitioner.cjs +10 -16
  8. package/dist/cjs/Transitioner.cjs.map +1 -1
  9. package/dist/cjs/headContentUtils.cjs +147 -59
  10. package/dist/cjs/headContentUtils.cjs.map +1 -1
  11. package/dist/cjs/index.cjs +1 -1
  12. package/dist/cjs/index.dev.cjs +1 -1
  13. package/dist/cjs/link.cjs +34 -29
  14. package/dist/cjs/link.cjs.map +1 -1
  15. package/dist/cjs/not-found.cjs +20 -2
  16. package/dist/cjs/not-found.cjs.map +1 -1
  17. package/dist/cjs/router.cjs +2 -1
  18. package/dist/cjs/router.cjs.map +1 -1
  19. package/dist/cjs/routerStores.cjs +21 -0
  20. package/dist/cjs/routerStores.cjs.map +1 -0
  21. package/dist/cjs/routerStores.d.cts +7 -0
  22. package/dist/cjs/ssr/RouterClient.cjs +1 -1
  23. package/dist/cjs/ssr/RouterClient.cjs.map +1 -1
  24. package/dist/cjs/ssr/renderRouterToStream.cjs +2 -2
  25. package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -1
  26. package/dist/cjs/ssr/renderRouterToString.cjs +1 -1
  27. package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -1
  28. package/dist/cjs/useCanGoBack.cjs +7 -2
  29. package/dist/cjs/useCanGoBack.cjs.map +1 -1
  30. package/dist/cjs/useLocation.cjs +21 -2
  31. package/dist/cjs/useLocation.cjs.map +1 -1
  32. package/dist/cjs/useMatch.cjs +29 -9
  33. package/dist/cjs/useMatch.cjs.map +1 -1
  34. package/dist/cjs/useRouterState.cjs +2 -2
  35. package/dist/cjs/useRouterState.cjs.map +1 -1
  36. package/dist/esm/Match.js +118 -52
  37. package/dist/esm/Match.js.map +1 -1
  38. package/dist/esm/Matches.js +21 -21
  39. package/dist/esm/Matches.js.map +1 -1
  40. package/dist/esm/Scripts.js +36 -32
  41. package/dist/esm/Scripts.js.map +1 -1
  42. package/dist/esm/Transitioner.js +10 -16
  43. package/dist/esm/Transitioner.js.map +1 -1
  44. package/dist/esm/headContentUtils.js +148 -60
  45. package/dist/esm/headContentUtils.js.map +1 -1
  46. package/dist/esm/index.dev.js +1 -1
  47. package/dist/esm/index.js +1 -1
  48. package/dist/esm/link.js +34 -29
  49. package/dist/esm/link.js.map +1 -1
  50. package/dist/esm/not-found.js +20 -2
  51. package/dist/esm/not-found.js.map +1 -1
  52. package/dist/esm/router.js +2 -1
  53. package/dist/esm/router.js.map +1 -1
  54. package/dist/esm/routerStores.d.ts +7 -0
  55. package/dist/esm/routerStores.js +20 -0
  56. package/dist/esm/routerStores.js.map +1 -0
  57. package/dist/esm/ssr/RouterClient.js +1 -1
  58. package/dist/esm/ssr/RouterClient.js.map +1 -1
  59. package/dist/esm/ssr/renderRouterToStream.js +2 -2
  60. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  61. package/dist/esm/ssr/renderRouterToString.js +1 -1
  62. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  63. package/dist/esm/useCanGoBack.js +6 -2
  64. package/dist/esm/useCanGoBack.js.map +1 -1
  65. package/dist/esm/useLocation.js +20 -2
  66. package/dist/esm/useLocation.js.map +1 -1
  67. package/dist/esm/useMatch.js +29 -9
  68. package/dist/esm/useMatch.js.map +1 -1
  69. package/dist/esm/useRouterState.js +2 -2
  70. package/dist/esm/useRouterState.js.map +1 -1
  71. package/dist/llms/rules/api.d.ts +1 -1
  72. package/dist/llms/rules/api.js +3 -9
  73. package/package.json +2 -2
  74. package/src/Match.tsx +218 -78
  75. package/src/Matches.tsx +45 -25
  76. package/src/Scripts.tsx +72 -44
  77. package/src/Transitioner.tsx +24 -16
  78. package/src/headContentUtils.tsx +210 -27
  79. package/src/link.tsx +66 -71
  80. package/src/not-found.tsx +41 -4
  81. package/src/router.ts +2 -1
  82. package/src/routerStores.ts +26 -0
  83. package/src/ssr/RouterClient.tsx +1 -1
  84. package/src/ssr/renderRouterToStream.tsx +2 -2
  85. package/src/ssr/renderRouterToString.tsx +1 -1
  86. package/src/useCanGoBack.ts +14 -2
  87. package/src/useLocation.tsx +32 -5
  88. package/src/useMatch.tsx +61 -21
  89. package/src/useRouterState.tsx +4 -2
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react'
2
+ import { batch, useStore } from '@tanstack/react-store'
2
3
  import {
3
4
  getLocationChangeInfo,
4
5
  handleHashScroll,
@@ -6,7 +7,6 @@ import {
6
7
  } from '@tanstack/router-core'
7
8
  import { useLayoutEffect, usePrevious } from './utils'
8
9
  import { useRouter } from './useRouter'
9
- import { useRouterState } from './useRouterState'
10
10
 
11
11
  export function Transitioner() {
12
12
  const router = useRouter()
@@ -14,13 +14,11 @@ export function Transitioner() {
14
14
 
15
15
  const [isTransitioning, setIsTransitioning] = React.useState(false)
16
16
  // Track pending state changes
17
- const { hasPendingMatches, isLoading } = useRouterState({
18
- select: (s) => ({
19
- isLoading: s.isLoading,
20
- hasPendingMatches: s.matches.some((d) => d.status === 'pending'),
21
- }),
22
- structuralSharing: true,
23
- })
17
+ const isLoading = useStore(router.stores.isLoading, (value) => value)
18
+ const hasPendingMatches = useStore(
19
+ router.stores.hasPendingMatches,
20
+ (value) => value,
21
+ )
24
22
 
25
23
  const previousIsLoading = usePrevious(isLoading)
26
24
 
@@ -95,7 +93,10 @@ export function Transitioner() {
95
93
  if (previousIsLoading && !isLoading) {
96
94
  router.emit({
97
95
  type: 'onLoad', // When the new URL has committed, when the new matches have been loaded into state.matches
98
- ...getLocationChangeInfo(router.state),
96
+ ...getLocationChangeInfo(
97
+ router.stores.location.state,
98
+ router.stores.resolvedLocation.state,
99
+ ),
99
100
  })
100
101
  }
101
102
  }, [previousIsLoading, router, isLoading])
@@ -105,24 +106,31 @@ export function Transitioner() {
105
106
  if (previousIsPagePending && !isPagePending) {
106
107
  router.emit({
107
108
  type: 'onBeforeRouteMount',
108
- ...getLocationChangeInfo(router.state),
109
+ ...getLocationChangeInfo(
110
+ router.stores.location.state,
111
+ router.stores.resolvedLocation.state,
112
+ ),
109
113
  })
110
114
  }
111
115
  }, [isPagePending, previousIsPagePending, router])
112
116
 
113
117
  useLayoutEffect(() => {
114
118
  if (previousIsAnyPending && !isAnyPending) {
115
- const changeInfo = getLocationChangeInfo(router.state)
119
+ const changeInfo = getLocationChangeInfo(
120
+ router.stores.location.state,
121
+ router.stores.resolvedLocation.state,
122
+ )
116
123
  router.emit({
117
124
  type: 'onResolved',
118
125
  ...changeInfo,
119
126
  })
120
127
 
121
- router.__store.setState((s: typeof router.state) => ({
122
- ...s,
123
- status: 'idle',
124
- resolvedLocation: s.location,
125
- }))
128
+ batch(() => {
129
+ router.stores.status.setState(() => 'idle')
130
+ router.stores.resolvedLocation.setState(
131
+ () => router.stores.location.state,
132
+ )
133
+ })
126
134
 
127
135
  if (changeInfo.hrefChanged) {
128
136
  handleHashScroll(router)
@@ -1,9 +1,171 @@
1
1
  import * as React from 'react'
2
- import { escapeHtml } from '@tanstack/router-core'
2
+ import { useStore } from '@tanstack/react-store'
3
+ import { deepEqual, escapeHtml } from '@tanstack/router-core'
4
+ import { isServer } from '@tanstack/router-core/isServer'
3
5
  import { useRouter } from './useRouter'
4
- import { useRouterState } from './useRouterState'
5
6
  import type { RouterManagedTag } from '@tanstack/router-core'
6
7
 
8
+ function buildTagsFromMatches(
9
+ router: ReturnType<typeof useRouter>,
10
+ nonce: string | undefined,
11
+ matches: Array<any>,
12
+ ): Array<RouterManagedTag> {
13
+ const routeMeta = matches.map((match) => match.meta!).filter(Boolean)
14
+
15
+ const resultMeta: Array<RouterManagedTag> = []
16
+ const metaByAttribute: Record<string, true> = {}
17
+ let title: RouterManagedTag | undefined
18
+ for (let i = routeMeta.length - 1; i >= 0; i--) {
19
+ const metas = routeMeta[i]!
20
+ for (let j = metas.length - 1; j >= 0; j--) {
21
+ const m = metas[j]
22
+ if (!m) continue
23
+
24
+ if (m.title) {
25
+ if (!title) {
26
+ title = {
27
+ tag: 'title',
28
+ children: m.title,
29
+ }
30
+ }
31
+ } else if ('script:ld+json' in m) {
32
+ try {
33
+ const json = JSON.stringify(m['script:ld+json'])
34
+ resultMeta.push({
35
+ tag: 'script',
36
+ attrs: {
37
+ type: 'application/ld+json',
38
+ },
39
+ children: escapeHtml(json),
40
+ })
41
+ } catch {
42
+ // Skip invalid JSON-LD objects
43
+ }
44
+ } else {
45
+ const attribute = m.name ?? m.property
46
+ if (attribute) {
47
+ if (metaByAttribute[attribute]) {
48
+ continue
49
+ } else {
50
+ metaByAttribute[attribute] = true
51
+ }
52
+ }
53
+
54
+ resultMeta.push({
55
+ tag: 'meta',
56
+ attrs: {
57
+ ...m,
58
+ nonce,
59
+ },
60
+ })
61
+ }
62
+ }
63
+ }
64
+
65
+ if (title) {
66
+ resultMeta.push(title)
67
+ }
68
+
69
+ if (nonce) {
70
+ resultMeta.push({
71
+ tag: 'meta',
72
+ attrs: {
73
+ property: 'csp-nonce',
74
+ content: nonce,
75
+ },
76
+ })
77
+ }
78
+ resultMeta.reverse()
79
+
80
+ const constructedLinks = matches
81
+ .map((match) => match.links!)
82
+ .filter(Boolean)
83
+ .flat(1)
84
+ .map((link) => ({
85
+ tag: 'link',
86
+ attrs: {
87
+ ...link,
88
+ nonce,
89
+ },
90
+ })) satisfies Array<RouterManagedTag>
91
+
92
+ const manifest = router.ssr?.manifest
93
+ const assetLinks = matches
94
+ .map((match) => manifest?.routes[match.routeId]?.assets ?? [])
95
+ .filter(Boolean)
96
+ .flat(1)
97
+ .filter((asset) => asset.tag === 'link')
98
+ .map(
99
+ (asset) =>
100
+ ({
101
+ tag: 'link',
102
+ attrs: {
103
+ ...asset.attrs,
104
+ suppressHydrationWarning: true,
105
+ nonce,
106
+ },
107
+ }) satisfies RouterManagedTag,
108
+ )
109
+
110
+ const preloadLinks: Array<RouterManagedTag> = []
111
+ matches
112
+ .map((match) => router.looseRoutesById[match.routeId]!)
113
+ .forEach((route) =>
114
+ router.ssr?.manifest?.routes[route.id]?.preloads
115
+ ?.filter(Boolean)
116
+ .forEach((preload) => {
117
+ preloadLinks.push({
118
+ tag: 'link',
119
+ attrs: {
120
+ rel: 'modulepreload',
121
+ href: preload,
122
+ nonce,
123
+ },
124
+ })
125
+ }),
126
+ )
127
+
128
+ const styles = (
129
+ matches
130
+ .map((match) => match.styles!)
131
+ .flat(1)
132
+ .filter(Boolean) as Array<RouterManagedTag>
133
+ ).map(({ children, ...attrs }) => ({
134
+ tag: 'style',
135
+ attrs: {
136
+ ...attrs,
137
+ nonce,
138
+ },
139
+ children,
140
+ }))
141
+
142
+ const headScripts = (
143
+ matches
144
+ .map((match) => match.headScripts!)
145
+ .flat(1)
146
+ .filter(Boolean) as Array<RouterManagedTag>
147
+ ).map(({ children, ...script }) => ({
148
+ tag: 'script',
149
+ attrs: {
150
+ ...script,
151
+ nonce,
152
+ },
153
+ children,
154
+ }))
155
+
156
+ return uniqBy(
157
+ [
158
+ ...resultMeta,
159
+ ...preloadLinks,
160
+ ...constructedLinks,
161
+ ...assetLinks,
162
+ ...styles,
163
+ ...headScripts,
164
+ ] as Array<RouterManagedTag>,
165
+ (d) => JSON.stringify(d),
166
+ )
167
+ }
168
+
7
169
  /**
8
170
  * Build the list of head/link/meta/script tags to render for active matches.
9
171
  * Used internally by `HeadContent`.
@@ -11,12 +173,25 @@ import type { RouterManagedTag } from '@tanstack/router-core'
11
173
  export const useTags = () => {
12
174
  const router = useRouter()
13
175
  const nonce = router.options.ssr?.nonce
14
- const routeMeta = useRouterState({
15
- select: (state) => {
16
- return state.matches.map((match) => match.meta!).filter(Boolean)
176
+
177
+ if (isServer ?? router.isServer) {
178
+ return buildTagsFromMatches(
179
+ router,
180
+ nonce,
181
+ router.stores.activeMatchesSnapshot.state,
182
+ )
183
+ }
184
+
185
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
186
+ const routeMeta = useStore(
187
+ router.stores.activeMatchesSnapshot,
188
+ (matches) => {
189
+ return matches.map((match) => match.meta!).filter(Boolean)
17
190
  },
18
- })
191
+ deepEqual,
192
+ )
19
193
 
194
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
20
195
  const meta: Array<RouterManagedTag> = React.useMemo(() => {
21
196
  const resultMeta: Array<RouterManagedTag> = []
22
197
  const metaByAttribute: Record<string, true> = {}
@@ -88,9 +263,11 @@ export const useTags = () => {
88
263
  return resultMeta
89
264
  }, [routeMeta, nonce])
90
265
 
91
- const links = useRouterState({
92
- select: (state) => {
93
- const constructed = state.matches
266
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
267
+ const links = useStore(
268
+ router.stores.activeMatchesSnapshot,
269
+ (matches) => {
270
+ const constructed = matches
94
271
  .map((match) => match.links!)
95
272
  .filter(Boolean)
96
273
  .flat(1)
@@ -106,7 +283,7 @@ export const useTags = () => {
106
283
 
107
284
  // These are the assets extracted from the ViteManifest
108
285
  // using the `startManifestPlugin`
109
- const assets = state.matches
286
+ const assets = matches
110
287
  .map((match) => manifest?.routes[match.routeId]?.assets ?? [])
111
288
  .filter(Boolean)
112
289
  .flat(1)
@@ -125,14 +302,16 @@ export const useTags = () => {
125
302
 
126
303
  return [...constructed, ...assets]
127
304
  },
128
- structuralSharing: true as any,
129
- })
305
+ deepEqual,
306
+ )
130
307
 
131
- const preloadLinks = useRouterState({
132
- select: (state) => {
308
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
309
+ const preloadLinks = useStore(
310
+ router.stores.activeMatchesSnapshot,
311
+ (matches) => {
133
312
  const preloadLinks: Array<RouterManagedTag> = []
134
313
 
135
- state.matches
314
+ matches
136
315
  .map((match) => router.looseRoutesById[match.routeId]!)
137
316
  .forEach((route) =>
138
317
  router.ssr?.manifest?.routes[route.id]?.preloads
@@ -151,13 +330,15 @@ export const useTags = () => {
151
330
 
152
331
  return preloadLinks
153
332
  },
154
- structuralSharing: true as any,
155
- })
333
+ deepEqual,
334
+ )
156
335
 
157
- const styles = useRouterState({
158
- select: (state) =>
336
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
337
+ const styles = useStore(
338
+ router.stores.activeMatchesSnapshot,
339
+ (matches) =>
159
340
  (
160
- state.matches
341
+ matches
161
342
  .map((match) => match.styles!)
162
343
  .flat(1)
163
344
  .filter(Boolean) as Array<RouterManagedTag>
@@ -169,13 +350,15 @@ export const useTags = () => {
169
350
  },
170
351
  children,
171
352
  })),
172
- structuralSharing: true as any,
173
- })
353
+ deepEqual,
354
+ )
174
355
 
175
- const headScripts: Array<RouterManagedTag> = useRouterState({
176
- select: (state) =>
356
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
357
+ const headScripts: Array<RouterManagedTag> = useStore(
358
+ router.stores.activeMatchesSnapshot,
359
+ (matches) =>
177
360
  (
178
- state.matches
361
+ matches
179
362
  .map((match) => match.headScripts!)
180
363
  .flat(1)
181
364
  .filter(Boolean) as Array<RouterManagedTag>
@@ -187,8 +370,8 @@ export const useTags = () => {
187
370
  },
188
371
  children,
189
372
  })),
190
- structuralSharing: true as any,
191
- })
373
+ deepEqual,
374
+ )
192
375
 
193
376
  return uniqBy(
194
377
  [
package/src/link.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react'
2
+ import { useStore } from '@tanstack/react-store'
2
3
  import { flushSync } from 'react-dom'
3
4
  import {
4
5
  deepEqual,
@@ -9,7 +10,6 @@ import {
9
10
  removeTrailingSlash,
10
11
  } from '@tanstack/router-core'
11
12
  import { isServer } from '@tanstack/router-core/isServer'
12
- import { useRouterState } from './useRouterState'
13
13
  import { useRouter } from './useRouter'
14
14
 
15
15
  import { useForwardedRef, useIntersectionObserver } from './utils'
@@ -102,7 +102,7 @@ export function useLinkProps<
102
102
  //
103
103
  // For SSR parity (to avoid hydration errors), we still compute the link's
104
104
  // active status on the server, but we avoid creating any router-state
105
- // subscriptions by reading from `router.state` directly.
105
+ // subscriptions by reading from the location store directly.
106
106
  //
107
107
  // Note: `location.hash` is not available on the server.
108
108
  // ==========================================================================
@@ -204,7 +204,7 @@ export function useLinkProps<
204
204
  const isActive = (() => {
205
205
  if (externalLink) return false
206
206
 
207
- const currentLocation = router.state.location
207
+ const currentLocation = router.stores.location.state
208
208
 
209
209
  const exact = activeOptions?.exact ?? false
210
210
 
@@ -377,32 +377,13 @@ export function useLinkProps<
377
377
  // eslint-disable-next-line react-hooks/rules-of-hooks
378
378
  const isHydrated = useHydrated()
379
379
 
380
- // subscribe to path/search/hash/params to re-build location when they change
381
- // eslint-disable-next-line react-hooks/rules-of-hooks
382
- const currentLocationState = useRouterState({
383
- select: (s) => {
384
- const leaf = s.matches[s.matches.length - 1]
385
- return {
386
- search: leaf?.search,
387
- hash: s.location.hash,
388
- path: leaf?.pathname, // path + params
389
- }
390
- },
391
- structuralSharing: true as any,
392
- })
393
-
394
- const from = options.from
395
-
396
380
  // eslint-disable-next-line react-hooks/rules-of-hooks
397
381
  const _options = React.useMemo(
398
- () => {
399
- return { ...options, from }
400
- },
382
+ () => options,
401
383
  // eslint-disable-next-line react-hooks/exhaustive-deps
402
384
  [
403
385
  router,
404
- currentLocationState,
405
- from,
386
+ options.from,
406
387
  options._fromLocation,
407
388
  options.hash,
408
389
  options.to,
@@ -415,11 +396,18 @@ export function useLinkProps<
415
396
  )
416
397
 
417
398
  // eslint-disable-next-line react-hooks/rules-of-hooks
418
- const next = React.useMemo(
419
- () => router.buildLocation({ ..._options } as any),
420
- [router, _options],
399
+ const currentLocation = useStore(
400
+ router.stores.location,
401
+ (l) => l,
402
+ (prev, next) => prev.href === next.href,
421
403
  )
422
404
 
405
+ // eslint-disable-next-line react-hooks/rules-of-hooks
406
+ const next = React.useMemo(() => {
407
+ const opts = { _fromLocation: currentLocation, ..._options }
408
+ return router.buildLocation(opts as any)
409
+ }, [router, currentLocation, _options])
410
+
423
411
  // Use publicHref - it contains the correct href for display
424
412
  // When a rewrite changes the origin, publicHref is the full URL
425
413
  // Otherwise it's the origin-stripped path
@@ -474,54 +462,61 @@ export function useLinkProps<
474
462
  }, [to, hrefOption, router.protocolAllowlist])
475
463
 
476
464
  // eslint-disable-next-line react-hooks/rules-of-hooks
477
- const isActive = useRouterState({
478
- select: (s) => {
479
- if (externalLink) return false
480
- if (activeOptions?.exact) {
481
- const testExact = exactPathTest(
482
- s.location.pathname,
483
- next.pathname,
484
- router.basepath,
485
- )
486
- if (!testExact) {
487
- return false
488
- }
489
- } else {
490
- const currentPathSplit = removeTrailingSlash(
491
- s.location.pathname,
492
- router.basepath,
493
- )
494
- const nextPathSplit = removeTrailingSlash(
495
- next.pathname,
496
- router.basepath,
497
- )
498
-
499
- const pathIsFuzzyEqual =
500
- currentPathSplit.startsWith(nextPathSplit) &&
501
- (currentPathSplit.length === nextPathSplit.length ||
502
- currentPathSplit[nextPathSplit.length] === '/')
503
-
504
- if (!pathIsFuzzyEqual) {
505
- return false
506
- }
465
+ const isActive = React.useMemo(() => {
466
+ if (externalLink) return false
467
+ if (activeOptions?.exact) {
468
+ const testExact = exactPathTest(
469
+ currentLocation.pathname,
470
+ next.pathname,
471
+ router.basepath,
472
+ )
473
+ if (!testExact) {
474
+ return false
507
475
  }
508
-
509
- if (activeOptions?.includeSearch ?? true) {
510
- const searchTest = deepEqual(s.location.search, next.search, {
511
- partial: !activeOptions?.exact,
512
- ignoreUndefined: !activeOptions?.explicitUndefined,
513
- })
514
- if (!searchTest) {
515
- return false
516
- }
476
+ } else {
477
+ const currentPathSplit = removeTrailingSlash(
478
+ currentLocation.pathname,
479
+ router.basepath,
480
+ )
481
+ const nextPathSplit = removeTrailingSlash(next.pathname, router.basepath)
482
+
483
+ const pathIsFuzzyEqual =
484
+ currentPathSplit.startsWith(nextPathSplit) &&
485
+ (currentPathSplit.length === nextPathSplit.length ||
486
+ currentPathSplit[nextPathSplit.length] === '/')
487
+
488
+ if (!pathIsFuzzyEqual) {
489
+ return false
517
490
  }
491
+ }
518
492
 
519
- if (activeOptions?.includeHash) {
520
- return isHydrated && s.location.hash === next.hash
493
+ if (activeOptions?.includeSearch ?? true) {
494
+ const searchTest = deepEqual(currentLocation.search, next.search, {
495
+ partial: !activeOptions?.exact,
496
+ ignoreUndefined: !activeOptions?.explicitUndefined,
497
+ })
498
+ if (!searchTest) {
499
+ return false
521
500
  }
522
- return true
523
- },
524
- })
501
+ }
502
+
503
+ if (activeOptions?.includeHash) {
504
+ return isHydrated && currentLocation.hash === next.hash
505
+ }
506
+ return true
507
+ }, [
508
+ activeOptions?.exact,
509
+ activeOptions?.explicitUndefined,
510
+ activeOptions?.includeHash,
511
+ activeOptions?.includeSearch,
512
+ currentLocation,
513
+ externalLink,
514
+ isHydrated,
515
+ next.hash,
516
+ next.pathname,
517
+ next.search,
518
+ router.basepath,
519
+ ])
525
520
 
526
521
  // Get the active props
527
522
  const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
package/src/not-found.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as React from 'react'
2
2
  import { isNotFound } from '@tanstack/router-core'
3
+ import { isServer } from '@tanstack/router-core/isServer'
4
+ import { useStore } from '@tanstack/react-store'
3
5
  import { CatchBoundary } from './CatchBoundary'
4
- import { useRouterState } from './useRouterState'
6
+ import { useRouter } from './useRouter'
5
7
  import type { ErrorInfo } from 'react'
6
8
  import type { NotFoundError } from '@tanstack/router-core'
7
9
 
@@ -10,10 +12,45 @@ export function CatchNotFound(props: {
10
12
  onCatch?: (error: Error, errorInfo: ErrorInfo) => void
11
13
  children: React.ReactNode
12
14
  }) {
15
+ const router = useRouter()
16
+
17
+ if (isServer ?? router.isServer) {
18
+ const pathname = router.stores.location.state.pathname
19
+ const status = router.stores.status.state
20
+ const resetKey = `not-found-${pathname}-${status}`
21
+
22
+ return (
23
+ <CatchBoundary
24
+ getResetKey={() => resetKey}
25
+ onCatch={(error, errorInfo) => {
26
+ if (isNotFound(error)) {
27
+ props.onCatch?.(error, errorInfo)
28
+ } else {
29
+ throw error
30
+ }
31
+ }}
32
+ errorComponent={({ error }) => {
33
+ if (isNotFound(error)) {
34
+ return props.fallback?.(error)
35
+ } else {
36
+ throw error
37
+ }
38
+ }}
39
+ >
40
+ {props.children}
41
+ </CatchBoundary>
42
+ )
43
+ }
44
+
13
45
  // TODO: Some way for the user to programmatically reset the not-found boundary?
14
- const resetKey = useRouterState({
15
- select: (s) => `not-found-${s.location.pathname}-${s.status}`,
16
- })
46
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
47
+ const pathname = useStore(
48
+ router.stores.location,
49
+ (location) => location.pathname,
50
+ )
51
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
52
+ const status = useStore(router.stores.status, (status) => status)
53
+ const resetKey = `not-found-${pathname}-${status}`
17
54
 
18
55
  return (
19
56
  <CatchBoundary
package/src/router.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { RouterCore } from '@tanstack/router-core'
2
2
  import { createFileRoute, createLazyFileRoute } from './fileRoute'
3
+ import { getStoreFactory } from './routerStores'
3
4
  import type { RouterHistory } from '@tanstack/history'
4
5
  import type {
5
6
  AnyRoute,
@@ -114,7 +115,7 @@ export class Router<
114
115
  TDehydrated
115
116
  >,
116
117
  ) {
117
- super(options)
118
+ super(options, getStoreFactory)
118
119
  }
119
120
  }
120
121
 
@@ -0,0 +1,26 @@
1
+ import { batch, createStore } from '@tanstack/react-store'
2
+ import {
3
+ createNonReactiveMutableStore,
4
+ createNonReactiveReadonlyStore,
5
+ } from '@tanstack/router-core'
6
+ import { isServer } from '@tanstack/router-core/isServer'
7
+ import type { Readable } from '@tanstack/react-store'
8
+ import type { GetStoreConfig } from '@tanstack/router-core'
9
+
10
+ declare module '@tanstack/router-core' {
11
+ export interface RouterReadableStore<TValue> extends Readable<TValue> {}
12
+ }
13
+ export const getStoreFactory: GetStoreConfig = (opts) => {
14
+ if (isServer ?? opts.isServer) {
15
+ return {
16
+ createMutableStore: createNonReactiveMutableStore,
17
+ createReadonlyStore: createNonReactiveReadonlyStore,
18
+ batch: (fn) => fn(),
19
+ }
20
+ }
21
+ return {
22
+ createMutableStore: createStore,
23
+ createReadonlyStore: createStore,
24
+ batch: batch,
25
+ }
26
+ }
@@ -7,7 +7,7 @@ let hydrationPromise: Promise<void | Array<Array<void>>> | undefined
7
7
 
8
8
  export function RouterClient(props: { router: AnyRouter }) {
9
9
  if (!hydrationPromise) {
10
- if (!props.router.state.matches.length) {
10
+ if (!props.router.stores.matchesId.state.length) {
11
11
  hydrationPromise = hydrate(props.router)
12
12
  } else {
13
13
  hydrationPromise = Promise.resolve()