@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
@@ -1841,7 +1841,6 @@ type RouterState = {
1841
1841
  isLoading: boolean
1842
1842
  isTransitioning: boolean
1843
1843
  matches: Array<RouteMatch>
1844
- pendingMatches: Array<RouteMatch>
1845
1844
  location: ParsedLocation
1846
1845
  resolvedLocation: ParsedLocation
1847
1846
  }
@@ -1871,11 +1870,6 @@ The \`RouterState\` type contains all of the properties that are available on th
1871
1870
  - Type: [\`Array<RouteMatch>\`](./RouteMatchType.md)
1872
1871
  - An array of all of the route matches that have been resolved and are currently active.
1873
1872
 
1874
- ### \`pendingMatches\` property
1875
-
1876
- - Type: [\`Array<RouteMatch>\`](./RouteMatchType.md)
1877
- - An array of all of the route matches that are currently pending.
1878
-
1879
1873
  ### \`location\` property
1880
1874
 
1881
1875
  - Type: [\`ParsedLocation\`](./ParsedLocationType.md)
@@ -3797,7 +3791,7 @@ function Component() {
3797
3791
  The \`useChildMatches\` hook returns all of the child [\`RouteMatch\`](./RouteMatchType.md) objects from the closest match down to the leaf-most match. **It does not include the current match, which can be obtained using the \`useMatch\` hook.**
3798
3792
 
3799
3793
  > [!IMPORTANT]
3800
- > If the router has pending matches and they are showing their pending component fallbacks, \`router.state.pendingMatches\` will used instead of \`router.state.matches\`.
3794
+ > If the router has pending matches and they are showing their pending component fallbacks, pending matches are used instead of active matches.
3801
3795
 
3802
3796
  ## useChildMatches options
3803
3797
 
@@ -4406,7 +4400,7 @@ function Component() {
4406
4400
  The \`useParentMatches\` hook returns all of the parent [\`RouteMatch\`](./RouteMatchType.md) objects from the root down to the immediate parent of the current match in context. **It does not include the current match, which can be obtained using the \`useMatch\` hook.**
4407
4401
 
4408
4402
  > [!IMPORTANT]
4409
- > If the router has pending matches and they are showing their pending component fallbacks, \`router.state.pendingMatches\` will used instead of \`router.state.matches\`.
4403
+ > If the router has pending matches and they are showing their pending component fallbacks, pending matches are used instead of active matches.
4410
4404
 
4411
4405
  ## useParentMatches options
4412
4406
 
@@ -4514,7 +4508,7 @@ function Component() {
4514
4508
  The \`useRouterState\` method is a hook that returns the current internal state of the router. This hook is useful for accessing the current state of the router in a component.
4515
4509
 
4516
4510
  > [!TIP]
4517
- > If you want to access the current location or the current matches, you should try out the [\`useLocation\`](./useLocationHook.md) and [\`useMatches\`](./useMatchesHook.md) hooks first. These hooks are designed to be more ergonomic and easier to use than accessing the router state directly.
4511
+ > If you want to access the current location or the current matches, you should try out the [\`useLocation\`](./useLocationHook.md) and [\`useMatches\`](./useMatchesHook.md) hooks first. These hooks are designed to be more performant, more ergonomic and easier to use than accessing the router state directly.
4518
4512
 
4519
4513
  ## useRouterState options
4520
4514
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.167.5",
3
+ "version": "1.168.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -82,7 +82,7 @@
82
82
  "tiny-invariant": "^1.3.3",
83
83
  "tiny-warning": "^1.0.3",
84
84
  "@tanstack/history": "1.161.6",
85
- "@tanstack/router-core": "1.167.5"
85
+ "@tanstack/router-core": "1.168.0"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@testing-library/jest-dom": "^6.6.3",
package/src/Match.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react'
2
+ import { useStore } from '@tanstack/react-store'
2
3
  import invariant from 'tiny-invariant'
3
4
  import warning from 'tiny-warning'
4
5
  import {
@@ -10,7 +11,6 @@ import {
10
11
  } from '@tanstack/router-core'
11
12
  import { isServer } from '@tanstack/router-core/isServer'
12
13
  import { CatchBoundary, ErrorComponent } from './CatchBoundary'
13
- import { useRouterState } from './useRouterState'
14
14
  import { useRouter } from './useRouter'
15
15
  import { CatchNotFound } from './not-found'
16
16
  import { matchContext } from './matchContext'
@@ -30,25 +30,88 @@ export const Match = React.memo(function MatchImpl({
30
30
  matchId: string
31
31
  }) {
32
32
  const router = useRouter()
33
- const matchState = useRouterState({
34
- select: (s) => {
35
- const matchIndex = s.matches.findIndex((d) => d.id === matchId)
36
- const match = s.matches[matchIndex]
37
- invariant(
38
- match,
39
- `Could not find match for matchId "${matchId}". Please file an issue!`,
40
- )
41
- return {
42
- routeId: match.routeId,
43
- ssr: match.ssr,
44
- _displayPending: match._displayPending,
45
- resetKey: s.loadedAt,
46
- parentRouteId: s.matches[matchIndex - 1]?.routeId as string,
47
- }
48
- },
49
- structuralSharing: true as any,
50
- })
51
33
 
34
+ if (isServer ?? router.isServer) {
35
+ const match = router.stores.activeMatchStoresById.get(matchId)?.state
36
+ invariant(
37
+ match,
38
+ `Could not find match for matchId "${matchId}". Please file an issue!`,
39
+ )
40
+
41
+ const routeId = match.routeId as string
42
+ const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
43
+ ?.id
44
+
45
+ return (
46
+ <MatchView
47
+ router={router}
48
+ matchId={matchId}
49
+ resetKey={router.stores.loadedAt.state}
50
+ matchState={{
51
+ routeId,
52
+ ssr: match.ssr,
53
+ _displayPending: match._displayPending,
54
+ parentRouteId,
55
+ }}
56
+ />
57
+ )
58
+ }
59
+
60
+ // Subscribe directly to the match store from the pool.
61
+ // The matchId prop is stable for this component's lifetime (set by Outlet),
62
+ // and reconcileMatchPool reuses stores for the same matchId.
63
+ // eslint-disable-next-line react-hooks/rules-of-hooks
64
+ const matchStore = router.stores.activeMatchStoresById.get(matchId)
65
+ invariant(
66
+ matchStore,
67
+ `Could not find match for matchId "${matchId}". Please file an issue!`,
68
+ )
69
+ // eslint-disable-next-line react-hooks/rules-of-hooks
70
+ const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
71
+ // eslint-disable-next-line react-hooks/rules-of-hooks
72
+ const match = useStore(matchStore, (value) => value)
73
+ // eslint-disable-next-line react-hooks/rules-of-hooks
74
+ const matchState = React.useMemo(() => {
75
+ const routeId = match.routeId as string
76
+ const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
77
+ ?.id
78
+
79
+ return {
80
+ routeId,
81
+ ssr: match.ssr,
82
+ _displayPending: match._displayPending,
83
+ parentRouteId: parentRouteId as string | undefined,
84
+ } satisfies MatchViewState
85
+ }, [match._displayPending, match.routeId, match.ssr, router.routesById])
86
+
87
+ return (
88
+ <MatchView
89
+ router={router}
90
+ matchId={matchId}
91
+ resetKey={resetKey}
92
+ matchState={matchState}
93
+ />
94
+ )
95
+ })
96
+
97
+ type MatchViewState = {
98
+ routeId: string
99
+ ssr: boolean | 'data-only' | undefined
100
+ _displayPending: boolean | undefined
101
+ parentRouteId: string | undefined
102
+ }
103
+
104
+ function MatchView({
105
+ router,
106
+ matchId,
107
+ resetKey,
108
+ matchState,
109
+ }: {
110
+ router: ReturnType<typeof useRouter>
111
+ matchId: string
112
+ resetKey: number
113
+ matchState: MatchViewState
114
+ }) {
52
115
  const route: AnyRoute = router.routesById[matchState.routeId]
53
116
 
54
117
  const PendingComponent =
@@ -94,7 +157,7 @@ export const Match = React.memo(function MatchImpl({
94
157
  <matchContext.Provider value={matchId}>
95
158
  <ResolvedSuspenseBoundary fallback={pendingElement}>
96
159
  <ResolvedCatchBoundary
97
- getResetKey={() => matchState.resetKey}
160
+ getResetKey={() => resetKey}
98
161
  errorComponent={routeErrorComponent || ErrorComponent}
99
162
  onCatch={(error, errorInfo) => {
100
163
  // Forward not found errors (we don't want to show the error component for these)
@@ -137,7 +200,7 @@ export const Match = React.memo(function MatchImpl({
137
200
  ) : null}
138
201
  </ShellComponent>
139
202
  )
140
- })
203
+ }
141
204
 
142
205
  // On Rendered can't happen above the root layout because it actually
143
206
  // renders a dummy dom element to track the rendered state of the app.
@@ -165,7 +228,10 @@ function OnRendered() {
165
228
  ) {
166
229
  router.emit({
167
230
  type: 'onRendered',
168
- ...getLocationChangeInfo(router.state),
231
+ ...getLocationChangeInfo(
232
+ router.stores.location.state,
233
+ router.stores.resolvedLocation.state,
234
+ ),
169
235
  })
170
236
  prevLocationRef.current = router.latestLocation
171
237
  }
@@ -181,39 +247,101 @@ export const MatchInner = React.memo(function MatchInnerImpl({
181
247
  }): any {
182
248
  const router = useRouter()
183
249
 
184
- const { match, key, routeId } = useRouterState({
185
- select: (s) => {
186
- const match = s.matches.find((d) => d.id === matchId)!
187
- const routeId = match.routeId as string
188
-
189
- const remountFn =
190
- (router.routesById[routeId] as AnyRoute).options.remountDeps ??
191
- router.options.defaultRemountDeps
192
- const remountDeps = remountFn?.({
193
- routeId,
194
- loaderDeps: match.loaderDeps,
195
- params: match._strictParams,
196
- search: match._strictSearch,
197
- })
198
- const key = remountDeps ? JSON.stringify(remountDeps) : undefined
199
-
200
- return {
201
- key,
202
- routeId,
203
- match: {
204
- id: match.id,
205
- status: match.status,
206
- error: match.error,
207
- _forcePending: match._forcePending,
208
- _displayPending: match._displayPending,
209
- },
210
- }
211
- },
212
- structuralSharing: true as any,
213
- })
250
+ if (isServer ?? router.isServer) {
251
+ const match = router.stores.activeMatchStoresById.get(matchId)?.state
252
+ invariant(
253
+ match,
254
+ `Could not find match for matchId "${matchId}". Please file an issue!`,
255
+ )
214
256
 
215
- const route = router.routesById[routeId] as AnyRoute
257
+ const routeId = match.routeId as string
258
+ const route = router.routesById[routeId] as AnyRoute
259
+ const remountFn =
260
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
261
+ router.options.defaultRemountDeps
262
+ const remountDeps = remountFn?.({
263
+ routeId,
264
+ loaderDeps: match.loaderDeps,
265
+ params: match._strictParams,
266
+ search: match._strictSearch,
267
+ })
268
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
269
+ const Comp = route.options.component ?? router.options.defaultComponent
270
+ const out = Comp ? <Comp key={key} /> : <Outlet />
271
+
272
+ if (match._displayPending) {
273
+ throw router.getMatch(match.id)?._nonReactive.displayPendingPromise
274
+ }
216
275
 
276
+ if (match._forcePending) {
277
+ throw router.getMatch(match.id)?._nonReactive.minPendingPromise
278
+ }
279
+
280
+ if (match.status === 'pending') {
281
+ throw router.getMatch(match.id)?._nonReactive.loadPromise
282
+ }
283
+
284
+ if (match.status === 'notFound') {
285
+ invariant(isNotFound(match.error), 'Expected a notFound error')
286
+ return renderRouteNotFound(router, route, match.error)
287
+ }
288
+
289
+ if (match.status === 'redirected') {
290
+ invariant(isRedirect(match.error), 'Expected a redirect error')
291
+ throw router.getMatch(match.id)?._nonReactive.loadPromise
292
+ }
293
+
294
+ if (match.status === 'error') {
295
+ const RouteErrorComponent =
296
+ (route.options.errorComponent ??
297
+ router.options.defaultErrorComponent) ||
298
+ ErrorComponent
299
+ return (
300
+ <RouteErrorComponent
301
+ error={match.error as any}
302
+ reset={undefined as any}
303
+ info={{
304
+ componentStack: '',
305
+ }}
306
+ />
307
+ )
308
+ }
309
+
310
+ return out
311
+ }
312
+
313
+ // eslint-disable-next-line react-hooks/rules-of-hooks
314
+ const matchStore = router.stores.activeMatchStoresById.get(matchId)
315
+ invariant(
316
+ matchStore,
317
+ `Could not find match for matchId "${matchId}". Please file an issue!`,
318
+ )
319
+ // eslint-disable-next-line react-hooks/rules-of-hooks
320
+ const match = useStore(matchStore, (value) => value)
321
+ const routeId = match.routeId as string
322
+ const route = router.routesById[routeId] as AnyRoute
323
+ // eslint-disable-next-line react-hooks/rules-of-hooks
324
+ const key = React.useMemo(() => {
325
+ const remountFn =
326
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
327
+ router.options.defaultRemountDeps
328
+ const remountDeps = remountFn?.({
329
+ routeId,
330
+ loaderDeps: match.loaderDeps,
331
+ params: match._strictParams,
332
+ search: match._strictSearch,
333
+ })
334
+ return remountDeps ? JSON.stringify(remountDeps) : undefined
335
+ }, [
336
+ routeId,
337
+ match.loaderDeps,
338
+ match._strictParams,
339
+ match._strictSearch,
340
+ router.options.defaultRemountDeps,
341
+ router.routesById,
342
+ ])
343
+
344
+ // eslint-disable-next-line react-hooks/rules-of-hooks
217
345
  const out = React.useMemo(() => {
218
346
  const Comp = route.options.component ?? router.options.defaultComponent
219
347
  if (Comp) {
@@ -309,37 +437,49 @@ export const MatchInner = React.memo(function MatchInnerImpl({
309
437
  export const Outlet = React.memo(function OutletImpl() {
310
438
  const router = useRouter()
311
439
  const matchId = React.useContext(matchContext)
312
- const routeId = useRouterState({
313
- select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
314
- })
315
-
316
- const route = router.routesById[routeId]!
317
-
318
- const parentGlobalNotFound = useRouterState({
319
- select: (s) => {
320
- const matches = s.matches
321
- const parentMatch = matches.find((d) => d.id === matchId)
322
- invariant(
323
- parentMatch,
324
- `Could not find parent match for matchId "${matchId}"`,
325
- )
326
- return parentMatch.globalNotFound
327
- },
328
- })
329
-
330
- const childMatchId = useRouterState({
331
- select: (s) => {
332
- const matches = s.matches
333
- const index = matches.findIndex((d) => d.id === matchId)
334
- return matches[index + 1]?.id
335
- },
336
- })
440
+
441
+ let routeId: string | undefined
442
+ let parentGlobalNotFound = false
443
+ let childMatchId: string | undefined
444
+
445
+ if (isServer ?? router.isServer) {
446
+ const matches = router.stores.activeMatchesSnapshot.state
447
+ const parentIndex = matchId
448
+ ? matches.findIndex((match) => match.id === matchId)
449
+ : -1
450
+ const parentMatch = parentIndex >= 0 ? matches[parentIndex] : undefined
451
+ routeId = parentMatch?.routeId as string | undefined
452
+ parentGlobalNotFound = parentMatch?.globalNotFound ?? false
453
+ childMatchId =
454
+ parentIndex >= 0 ? (matches[parentIndex + 1]?.id as string) : undefined
455
+ } else {
456
+ // Subscribe directly to the match store from the pool instead of
457
+ // the two-level byId → matchStore pattern.
458
+ const parentMatchStore = matchId
459
+ ? router.stores.activeMatchStoresById.get(matchId)
460
+ : undefined
461
+
462
+ // eslint-disable-next-line react-hooks/rules-of-hooks
463
+ ;[routeId, parentGlobalNotFound] = useStore(parentMatchStore, (match) => [
464
+ match?.routeId as string | undefined,
465
+ match?.globalNotFound ?? false,
466
+ ])
467
+
468
+ // eslint-disable-next-line react-hooks/rules-of-hooks
469
+ childMatchId = useStore(router.stores.matchesId, (ids) => {
470
+ const index = ids.findIndex((id) => id === matchId)
471
+ return ids[index + 1]
472
+ })
473
+ }
474
+
475
+ const route = routeId ? router.routesById[routeId] : undefined
337
476
 
338
477
  const pendingElement = router.options.defaultPendingComponent ? (
339
478
  <router.options.defaultPendingComponent />
340
479
  ) : null
341
480
 
342
481
  if (parentGlobalNotFound) {
482
+ invariant(route, 'Could not resolve route for Outlet render')
343
483
  return renderRouteNotFound(router, route, undefined)
344
484
  }
345
485
 
package/src/Matches.tsx CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as React from 'react'
2
2
  import warning from 'tiny-warning'
3
- import { rootRouteId } from '@tanstack/router-core'
3
+ import { useStore } from '@tanstack/react-store'
4
+ import { replaceEqualDeep, rootRouteId } from '@tanstack/router-core'
4
5
  import { isServer } from '@tanstack/router-core/isServer'
5
6
  import { CatchBoundary, ErrorComponent } from './CatchBoundary'
6
- import { useRouterState } from './useRouterState'
7
7
  import { useRouter } from './useRouter'
8
8
  import { Transitioner } from './Transitioner'
9
9
  import { matchContext } from './matchContext'
@@ -28,7 +28,6 @@ import type {
28
28
  ResolveRelativePath,
29
29
  ResolveRoute,
30
30
  RouteByPath,
31
- RouterState,
32
31
  ToSubOptionsProps,
33
32
  } from '@tanstack/router-core'
34
33
 
@@ -78,15 +77,15 @@ export function Matches() {
78
77
 
79
78
  function MatchesInner() {
80
79
  const router = useRouter()
81
- const matchId = useRouterState({
82
- select: (s) => {
83
- return s.matches[0]?.id
84
- },
85
- })
86
-
87
- const resetKey = useRouterState({
88
- select: (s) => s.loadedAt,
89
- })
80
+ const _isServer = isServer ?? router.isServer
81
+ const matchId = _isServer
82
+ ? router.stores.firstMatchId.state
83
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
84
+ useStore(router.stores.firstMatchId, (id) => id)
85
+ const resetKey = _isServer
86
+ ? router.stores.loadedAt.state
87
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
88
+ useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
90
89
 
91
90
  const matchComponent = matchId ? <Match matchId={matchId} /> : null
92
91
 
@@ -144,10 +143,10 @@ export type UseMatchRouteOptions<
144
143
  export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
145
144
  const router = useRouter()
146
145
 
147
- useRouterState({
148
- select: (s) => [s.location.href, s.resolvedLocation?.href, s.status],
149
- structuralSharing: true as any,
150
- })
146
+ if (!(isServer ?? router.isServer)) {
147
+ // eslint-disable-next-line react-hooks/rules-of-hooks
148
+ useStore(router.stores.matchRouteReactivity, (d) => d)
149
+ }
151
150
 
152
151
  return React.useCallback(
153
152
  <
@@ -238,15 +237,36 @@ export function useMatches<
238
237
  opts?: UseMatchesBaseOptions<TRouter, TSelected, TStructuralSharing> &
239
238
  StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
240
239
  ): UseMatchesResult<TRouter, TSelected> {
241
- return useRouterState({
242
- select: (state: RouterState<TRouter['routeTree']>) => {
243
- const matches = state.matches
244
- return opts?.select
245
- ? opts.select(matches as Array<MakeRouteMatchUnion<TRouter>>)
246
- : matches
247
- },
248
- structuralSharing: opts?.structuralSharing,
249
- } as any) as UseMatchesResult<TRouter, TSelected>
240
+ const router = useRouter<TRouter>()
241
+ const previousResult =
242
+ React.useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(
243
+ undefined,
244
+ )
245
+
246
+ if (isServer ?? router.isServer) {
247
+ const matches = router.stores.activeMatchesSnapshot.state as Array<
248
+ MakeRouteMatchUnion<TRouter>
249
+ >
250
+ return (opts?.select ? opts.select(matches) : matches) as UseMatchesResult<
251
+ TRouter,
252
+ TSelected
253
+ >
254
+ }
255
+
256
+ // eslint-disable-next-line react-hooks/rules-of-hooks
257
+ return useStore(router.stores.activeMatchesSnapshot, (matches) => {
258
+ const selected = opts?.select
259
+ ? opts.select(matches as Array<MakeRouteMatchUnion<TRouter>>)
260
+ : (matches as any)
261
+
262
+ if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
263
+ const shared = replaceEqualDeep(previousResult.current, selected)
264
+ previousResult.current = shared
265
+ return shared
266
+ }
267
+
268
+ return selected
269
+ }) as UseMatchesResult<TRouter, TSelected>
250
270
  }
251
271
 
252
272
  /**
package/src/Scripts.tsx CHANGED
@@ -1,5 +1,7 @@
1
+ import { useStore } from '@tanstack/react-store'
2
+ import { deepEqual } from '@tanstack/router-core'
3
+ import { isServer } from '@tanstack/router-core/isServer'
1
4
  import { Asset } from './Asset'
2
- import { useRouterState } from './useRouterState'
3
5
  import { useRouter } from './useRouter'
4
6
  import type { RouterManagedTag } from '@tanstack/router-core'
5
7
 
@@ -10,54 +12,80 @@ import type { RouterManagedTag } from '@tanstack/router-core'
10
12
  export const Scripts = () => {
11
13
  const router = useRouter()
12
14
  const nonce = router.options.ssr?.nonce
13
- const assetScripts = useRouterState({
14
- select: (state) => {
15
- const assetScripts: Array<RouterManagedTag> = []
16
- const manifest = router.ssr?.manifest
17
15
 
18
- if (!manifest) {
19
- return []
20
- }
16
+ const getAssetScripts = (matches: Array<any>) => {
17
+ const assetScripts: Array<RouterManagedTag> = []
18
+ const manifest = router.ssr?.manifest
21
19
 
22
- state.matches
23
- .map((match) => router.looseRoutesById[match.routeId]!)
24
- .forEach((route) =>
25
- manifest.routes[route.id]?.assets
26
- ?.filter((d) => d.tag === 'script')
27
- .forEach((asset) => {
28
- assetScripts.push({
29
- tag: 'script',
30
- attrs: { ...asset.attrs, nonce },
31
- children: asset.children,
32
- } as any)
33
- }),
34
- )
20
+ if (!manifest) {
21
+ return []
22
+ }
35
23
 
36
- return assetScripts
37
- },
38
- structuralSharing: true as any,
39
- })
24
+ matches
25
+ .map((match) => router.looseRoutesById[match.routeId]!)
26
+ .forEach((route) =>
27
+ manifest.routes[route.id]?.assets
28
+ ?.filter((d) => d.tag === 'script')
29
+ .forEach((asset) => {
30
+ assetScripts.push({
31
+ tag: 'script',
32
+ attrs: { ...asset.attrs, nonce },
33
+ children: asset.children,
34
+ } as any)
35
+ }),
36
+ )
40
37
 
41
- const { scripts } = useRouterState({
42
- select: (state) => ({
43
- scripts: (
44
- state.matches
45
- .map((match) => match.scripts!)
46
- .flat(1)
47
- .filter(Boolean) as Array<RouterManagedTag>
48
- ).map(({ children, ...script }) => ({
49
- tag: 'script',
50
- attrs: {
51
- ...script,
52
- suppressHydrationWarning: true,
53
- nonce,
54
- },
55
- children,
56
- })),
57
- }),
58
- structuralSharing: true as any,
59
- })
38
+ return assetScripts
39
+ }
40
+
41
+ const getScripts = (matches: Array<any>): Array<RouterManagedTag> =>
42
+ (
43
+ matches
44
+ .map((match) => match.scripts!)
45
+ .flat(1)
46
+ .filter(Boolean) as Array<RouterManagedTag>
47
+ ).map(
48
+ ({ children, ...script }) =>
49
+ ({
50
+ tag: 'script',
51
+ attrs: {
52
+ ...script,
53
+ suppressHydrationWarning: true,
54
+ nonce,
55
+ },
56
+ children,
57
+ }) satisfies RouterManagedTag,
58
+ )
59
+
60
+ if (isServer ?? router.isServer) {
61
+ const assetScripts = getAssetScripts(
62
+ router.stores.activeMatchesSnapshot.state,
63
+ )
64
+ const scripts = getScripts(router.stores.activeMatchesSnapshot.state)
65
+ return renderScripts(router, scripts, assetScripts)
66
+ }
67
+
68
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
69
+ const assetScripts = useStore(
70
+ router.stores.activeMatchesSnapshot,
71
+ getAssetScripts,
72
+ deepEqual,
73
+ )
74
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
75
+ const scripts = useStore(
76
+ router.stores.activeMatchesSnapshot,
77
+ getScripts,
78
+ deepEqual,
79
+ )
80
+
81
+ return renderScripts(router, scripts, assetScripts)
82
+ }
60
83
 
84
+ function renderScripts(
85
+ router: ReturnType<typeof useRouter>,
86
+ scripts: Array<RouterManagedTag>,
87
+ assetScripts: Array<RouterManagedTag>,
88
+ ) {
61
89
  let serverBufferedScript: RouterManagedTag | undefined = undefined
62
90
 
63
91
  if (router.serverSsr) {