@depup/tanstack__react-router 1.167.5-depup.0 → 1.168.2-depup.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 (113) hide show
  1. package/README.md +3 -4
  2. package/changes.json +2 -6
  3. package/dist/cjs/Match.cjs +147 -58
  4. package/dist/cjs/Match.cjs.map +1 -1
  5. package/dist/cjs/Matches.cjs +22 -24
  6. package/dist/cjs/Matches.cjs.map +1 -1
  7. package/dist/cjs/Scripts.cjs +36 -32
  8. package/dist/cjs/Scripts.cjs.map +1 -1
  9. package/dist/cjs/Transitioner.cjs +10 -16
  10. package/dist/cjs/Transitioner.cjs.map +1 -1
  11. package/dist/cjs/fileRoute.cjs +4 -6
  12. package/dist/cjs/fileRoute.cjs.map +1 -1
  13. package/dist/cjs/headContentUtils.cjs +147 -59
  14. package/dist/cjs/headContentUtils.cjs.map +1 -1
  15. package/dist/cjs/index.cjs +1 -1
  16. package/dist/cjs/index.dev.cjs +1 -1
  17. package/dist/cjs/link.cjs +34 -29
  18. package/dist/cjs/link.cjs.map +1 -1
  19. package/dist/cjs/not-found.cjs +20 -2
  20. package/dist/cjs/not-found.cjs.map +1 -1
  21. package/dist/cjs/renderRouteNotFound.cjs +3 -3
  22. package/dist/cjs/renderRouteNotFound.cjs.map +1 -1
  23. package/dist/cjs/route.cjs +0 -2
  24. package/dist/cjs/route.cjs.map +1 -1
  25. package/dist/cjs/router.cjs +2 -1
  26. package/dist/cjs/router.cjs.map +1 -1
  27. package/dist/cjs/routerStores.cjs +21 -0
  28. package/dist/cjs/routerStores.cjs.map +1 -0
  29. package/dist/cjs/routerStores.d.cts +7 -0
  30. package/dist/cjs/ssr/RouterClient.cjs +1 -1
  31. package/dist/cjs/ssr/RouterClient.cjs.map +1 -1
  32. package/dist/cjs/ssr/renderRouterToStream.cjs +2 -2
  33. package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -1
  34. package/dist/cjs/ssr/renderRouterToString.cjs +1 -1
  35. package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -1
  36. package/dist/cjs/useCanGoBack.cjs +7 -2
  37. package/dist/cjs/useCanGoBack.cjs.map +1 -1
  38. package/dist/cjs/useLocation.cjs +21 -2
  39. package/dist/cjs/useLocation.cjs.map +1 -1
  40. package/dist/cjs/useMatch.cjs +35 -11
  41. package/dist/cjs/useMatch.cjs.map +1 -1
  42. package/dist/cjs/useRouter.cjs +3 -3
  43. package/dist/cjs/useRouter.cjs.map +1 -1
  44. package/dist/cjs/useRouterState.cjs +2 -2
  45. package/dist/cjs/useRouterState.cjs.map +1 -1
  46. package/dist/esm/Match.js +148 -57
  47. package/dist/esm/Match.js.map +1 -1
  48. package/dist/esm/Matches.js +23 -24
  49. package/dist/esm/Matches.js.map +1 -1
  50. package/dist/esm/Scripts.js +36 -32
  51. package/dist/esm/Scripts.js.map +1 -1
  52. package/dist/esm/Transitioner.js +10 -16
  53. package/dist/esm/Transitioner.js.map +1 -1
  54. package/dist/esm/fileRoute.js +4 -4
  55. package/dist/esm/fileRoute.js.map +1 -1
  56. package/dist/esm/headContentUtils.js +148 -60
  57. package/dist/esm/headContentUtils.js.map +1 -1
  58. package/dist/esm/index.dev.js +1 -1
  59. package/dist/esm/index.js +1 -1
  60. package/dist/esm/link.js +34 -29
  61. package/dist/esm/link.js.map +1 -1
  62. package/dist/esm/not-found.js +20 -2
  63. package/dist/esm/not-found.js.map +1 -1
  64. package/dist/esm/renderRouteNotFound.js +3 -2
  65. package/dist/esm/renderRouteNotFound.js.map +1 -1
  66. package/dist/esm/route.js +0 -2
  67. package/dist/esm/route.js.map +1 -1
  68. package/dist/esm/router.js +2 -1
  69. package/dist/esm/router.js.map +1 -1
  70. package/dist/esm/routerStores.d.ts +7 -0
  71. package/dist/esm/routerStores.js +20 -0
  72. package/dist/esm/routerStores.js.map +1 -0
  73. package/dist/esm/ssr/RouterClient.js +1 -1
  74. package/dist/esm/ssr/RouterClient.js.map +1 -1
  75. package/dist/esm/ssr/renderRouterToStream.js +2 -2
  76. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  77. package/dist/esm/ssr/renderRouterToString.js +1 -1
  78. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  79. package/dist/esm/useCanGoBack.js +6 -2
  80. package/dist/esm/useCanGoBack.js.map +1 -1
  81. package/dist/esm/useLocation.js +20 -2
  82. package/dist/esm/useLocation.js.map +1 -1
  83. package/dist/esm/useMatch.js +35 -10
  84. package/dist/esm/useMatch.js.map +1 -1
  85. package/dist/esm/useRouter.js +3 -2
  86. package/dist/esm/useRouter.js.map +1 -1
  87. package/dist/esm/useRouterState.js +2 -2
  88. package/dist/esm/useRouterState.js.map +1 -1
  89. package/dist/llms/rules/api.d.ts +1 -1
  90. package/dist/llms/rules/api.js +13 -19
  91. package/dist/llms/rules/guide.d.ts +1 -1
  92. package/dist/llms/rules/guide.js +27 -6
  93. package/package.json +5 -11
  94. package/src/Match.tsx +274 -81
  95. package/src/Matches.tsx +48 -30
  96. package/src/Scripts.tsx +72 -44
  97. package/src/Transitioner.tsx +24 -16
  98. package/src/fileRoute.ts +7 -9
  99. package/src/headContentUtils.tsx +210 -27
  100. package/src/link.tsx +66 -71
  101. package/src/not-found.tsx +41 -4
  102. package/src/renderRouteNotFound.tsx +6 -6
  103. package/src/route.tsx +0 -2
  104. package/src/router.ts +2 -1
  105. package/src/routerStores.ts +26 -0
  106. package/src/ssr/RouterClient.tsx +1 -1
  107. package/src/ssr/renderRouterToStream.tsx +2 -2
  108. package/src/ssr/renderRouterToString.tsx +1 -1
  109. package/src/useCanGoBack.ts +14 -2
  110. package/src/useLocation.tsx +32 -5
  111. package/src/useMatch.tsx +68 -19
  112. package/src/useRouter.tsx +7 -5
  113. package/src/useRouterState.tsx +4 -2
@@ -8384,7 +8384,9 @@ For validation libraries we recommend using adapters which infer the correct \`i
8384
8384
 
8385
8385
  ### Zod
8386
8386
 
8387
- An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct \`input\` type and \`output\` type
8387
+ An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct \`input\` type and \`output\` type.
8388
+
8389
+ For Zod v3:
8388
8390
 
8389
8391
  \`\`\`tsx
8390
8392
  import { zodValidator } from '@tanstack/zod-adapter'
@@ -8401,13 +8403,30 @@ export const Route = createFileRoute('/shop/products/')({
8401
8403
  })
8402
8404
  \`\`\`
8403
8405
 
8404
- The important part here is the following use of \`Link\` no longer requires \`search\` params
8406
+ With Zod v4, you should directly use the schema in \`validateSearch\`:
8407
+
8408
+ \`\`\`tsx
8409
+ import { z } from 'zod'
8410
+
8411
+ const productSearchSchema = z.object({
8412
+ page: z.number().default(1),
8413
+ filter: z.string().default(''),
8414
+ sort: z.enum(['newest', 'oldest', 'price']).default('newest'),
8415
+ })
8416
+
8417
+ export const Route = createFileRoute('/shop/products/')({
8418
+ // With Zod v4, we can use the schema without the adapter
8419
+ validateSearch: productSearchSchema,
8420
+ })
8421
+ \`\`\`
8422
+
8423
+ The important part here is the following use of \`Link\` no longer requires \`search\` params:
8405
8424
 
8406
8425
  \`\`\`tsx
8407
8426
  <Link to="/shop/products" />
8408
8427
  \`\`\`
8409
8428
 
8410
- However the use of \`catch\` here overrides the types and makes \`page\`, \`filter\` and \`sort\` \`unknown\` causing type loss. We have handled this case by providing a \`fallback\` generic function which retains the types but provides a \`fallback\` value when validation fails
8429
+ In Zod v3, the use of \`catch\` here overrides the types and makes \`page\`, \`filter\` and \`sort\` \`unknown\` causing type loss. We have handled this case by providing a \`fallback\` generic function which retains the types but provides a \`fallback\` value when validation fails:
8411
8430
 
8412
8431
  \`\`\`tsx
8413
8432
  import { fallback, zodValidator } from '@tanstack/zod-adapter'
@@ -8428,7 +8447,9 @@ export const Route = createFileRoute('/shop/products/')({
8428
8447
 
8429
8448
  Therefore when navigating to this route, \`search\` is optional and retains the correct types.
8430
8449
 
8431
- While not recommended, it is also possible to configure \`input\` and \`output\` type in case the \`output\` type is more accurate than the \`input\` type
8450
+ In Zod v4, schemas may use \`catch\` instead of the fallback and will retain type inference throughout.
8451
+
8452
+ While not recommended, it is also possible to configure \`input\` and \`output\` type in case the \`output\` type is more accurate than the \`input\` type:
8432
8453
 
8433
8454
  \`\`\`tsx
8434
8455
  const productSearchSchema = z.object({
@@ -8645,7 +8666,7 @@ Now that you've learned how to read your route's search params, you'll be happy
8645
8666
 
8646
8667
  The best way to update search params is to use the \`search\` prop on the \`<Link />\` component.
8647
8668
 
8648
- If the search for the current page shall be updated and the \`from\` prop is specified, the \`to\` prop can be omitted.
8669
+ If the search for the current page shall be updated and the \`from\` prop is specified, the \`to\` prop can be omitted.
8649
8670
  Here's an example:
8650
8671
 
8651
8672
  \`\`\`tsx title="src/routes/shop/products.tsx"
@@ -8666,7 +8687,7 @@ const ProductList = () => {
8666
8687
 
8667
8688
  If you want to update the search params in a generic component that is rendered on multiple routes, specifying \`from\` can be challenging.
8668
8689
 
8669
- In this scenario you can set \`to="."\` which will give you access to loosely typed search params.
8690
+ In this scenario you can set \`to="."\` which will give you access to loosely typed search params.
8670
8691
  Here is an example that illustrates this:
8671
8692
 
8672
8693
  \`\`\`tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depup/tanstack__react-router",
3
- "version": "1.167.5-depup.0",
3
+ "version": "1.168.2-depup.0",
4
4
  "description": "Modern and scalable routing for React applications (with updated dependencies)",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -87,10 +87,8 @@
87
87
  "dependencies": {
88
88
  "@tanstack/react-store": "^0.9.2",
89
89
  "isbot": "^5.1.36",
90
- "tiny-invariant": "^1.3.3",
91
- "tiny-warning": "^1.0.3",
92
90
  "@tanstack/history": "1.161.6",
93
- "@tanstack/router-core": "1.167.5"
91
+ "@tanstack/router-core": "1.168.2"
94
92
  },
95
93
  "devDependencies": {
96
94
  "@testing-library/jest-dom": "^6.6.3",
@@ -128,19 +126,15 @@
128
126
  },
129
127
  "depup": {
130
128
  "changes": {
131
- "@tanstack/react-store": {
132
- "from": "^0.9.1",
133
- "to": "^0.9.2"
134
- },
135
129
  "isbot": {
136
130
  "from": "^5.1.22",
137
131
  "to": "^5.1.36"
138
132
  }
139
133
  },
140
- "depsUpdated": 2,
134
+ "depsUpdated": 1,
141
135
  "originalPackage": "@tanstack/react-router",
142
- "originalVersion": "1.167.5",
143
- "processedAt": "2026-03-18T17:27:34.273Z",
136
+ "originalVersion": "1.168.2",
137
+ "processedAt": "2026-03-22T12:15:25.312Z",
144
138
  "smokeTest": "passed"
145
139
  }
146
140
  }
package/src/Match.tsx CHANGED
@@ -1,16 +1,15 @@
1
1
  import * as React from 'react'
2
- import invariant from 'tiny-invariant'
3
- import warning from 'tiny-warning'
2
+ import { useStore } from '@tanstack/react-store'
4
3
  import {
5
4
  createControlledPromise,
6
5
  getLocationChangeInfo,
6
+ invariant,
7
7
  isNotFound,
8
8
  isRedirect,
9
9
  rootRouteId,
10
10
  } from '@tanstack/router-core'
11
11
  import { isServer } from '@tanstack/router-core/isServer'
12
12
  import { CatchBoundary, ErrorComponent } from './CatchBoundary'
13
- import { useRouterState } from './useRouterState'
14
13
  import { useRouter } from './useRouter'
15
14
  import { CatchNotFound } from './not-found'
16
15
  import { matchContext } from './matchContext'
@@ -30,25 +29,98 @@ export const Match = React.memo(function MatchImpl({
30
29
  matchId: string
31
30
  }) {
32
31
  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,
32
+
33
+ if (isServer ?? router.isServer) {
34
+ const match = router.stores.activeMatchStoresById.get(matchId)?.state
35
+ if (!match) {
36
+ if (process.env.NODE_ENV !== 'production') {
37
+ throw new Error(
38
+ `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
39
+ )
47
40
  }
48
- },
49
- structuralSharing: true as any,
50
- })
51
41
 
42
+ invariant()
43
+ }
44
+
45
+ const routeId = match.routeId as string
46
+ const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
47
+ ?.id
48
+
49
+ return (
50
+ <MatchView
51
+ router={router}
52
+ matchId={matchId}
53
+ resetKey={router.stores.loadedAt.state}
54
+ matchState={{
55
+ routeId,
56
+ ssr: match.ssr,
57
+ _displayPending: match._displayPending,
58
+ parentRouteId,
59
+ }}
60
+ />
61
+ )
62
+ }
63
+
64
+ // Subscribe directly to the match store from the pool.
65
+ // The matchId prop is stable for this component's lifetime (set by Outlet),
66
+ // and reconcileMatchPool reuses stores for the same matchId.
67
+ // eslint-disable-next-line react-hooks/rules-of-hooks
68
+ const matchStore = router.stores.activeMatchStoresById.get(matchId)
69
+ if (!matchStore) {
70
+ if (process.env.NODE_ENV !== 'production') {
71
+ throw new Error(
72
+ `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
73
+ )
74
+ }
75
+
76
+ invariant()
77
+ }
78
+ // eslint-disable-next-line react-hooks/rules-of-hooks
79
+ const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
80
+ // eslint-disable-next-line react-hooks/rules-of-hooks
81
+ const match = useStore(matchStore, (value) => value)
82
+ // eslint-disable-next-line react-hooks/rules-of-hooks
83
+ const matchState = React.useMemo(() => {
84
+ const routeId = match.routeId as string
85
+ const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
86
+ ?.id
87
+
88
+ return {
89
+ routeId,
90
+ ssr: match.ssr,
91
+ _displayPending: match._displayPending,
92
+ parentRouteId: parentRouteId as string | undefined,
93
+ } satisfies MatchViewState
94
+ }, [match._displayPending, match.routeId, match.ssr, router.routesById])
95
+
96
+ return (
97
+ <MatchView
98
+ router={router}
99
+ matchId={matchId}
100
+ resetKey={resetKey}
101
+ matchState={matchState}
102
+ />
103
+ )
104
+ })
105
+
106
+ type MatchViewState = {
107
+ routeId: string
108
+ ssr: boolean | 'data-only' | undefined
109
+ _displayPending: boolean | undefined
110
+ parentRouteId: string | undefined
111
+ }
112
+
113
+ function MatchView({
114
+ router,
115
+ matchId,
116
+ resetKey,
117
+ matchState,
118
+ }: {
119
+ router: ReturnType<typeof useRouter>
120
+ matchId: string
121
+ resetKey: number
122
+ matchState: MatchViewState
123
+ }) {
52
124
  const route: AnyRoute = router.routesById[matchState.routeId]
53
125
 
54
126
  const PendingComponent =
@@ -94,12 +166,14 @@ export const Match = React.memo(function MatchImpl({
94
166
  <matchContext.Provider value={matchId}>
95
167
  <ResolvedSuspenseBoundary fallback={pendingElement}>
96
168
  <ResolvedCatchBoundary
97
- getResetKey={() => matchState.resetKey}
169
+ getResetKey={() => resetKey}
98
170
  errorComponent={routeErrorComponent || ErrorComponent}
99
171
  onCatch={(error, errorInfo) => {
100
172
  // Forward not found errors (we don't want to show the error component for these)
101
173
  if (isNotFound(error)) throw error
102
- warning(false, `Error in route match: ${matchId}`)
174
+ if (process.env.NODE_ENV !== 'production') {
175
+ console.warn(`Warning: Error in route match: ${matchId}`)
176
+ }
103
177
  routeOnCatch?.(error, errorInfo)
104
178
  }}
105
179
  >
@@ -137,7 +211,7 @@ export const Match = React.memo(function MatchImpl({
137
211
  ) : null}
138
212
  </ShellComponent>
139
213
  )
140
- })
214
+ }
141
215
 
142
216
  // On Rendered can't happen above the root layout because it actually
143
217
  // renders a dummy dom element to track the rendered state of the app.
@@ -165,7 +239,10 @@ function OnRendered() {
165
239
  ) {
166
240
  router.emit({
167
241
  type: 'onRendered',
168
- ...getLocationChangeInfo(router.state),
242
+ ...getLocationChangeInfo(
243
+ router.stores.location.state,
244
+ router.stores.resolvedLocation.state,
245
+ ),
169
246
  })
170
247
  prevLocationRef.current = router.latestLocation
171
248
  }
@@ -181,39 +258,123 @@ export const MatchInner = React.memo(function MatchInnerImpl({
181
258
  }): any {
182
259
  const router = useRouter()
183
260
 
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
- },
261
+ if (isServer ?? router.isServer) {
262
+ const match = router.stores.activeMatchStoresById.get(matchId)?.state
263
+ if (!match) {
264
+ if (process.env.NODE_ENV !== 'production') {
265
+ throw new Error(
266
+ `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
267
+ )
210
268
  }
211
- },
212
- structuralSharing: true as any,
213
- })
214
269
 
215
- const route = router.routesById[routeId] as AnyRoute
270
+ invariant()
271
+ }
272
+
273
+ const routeId = match.routeId as string
274
+ const route = router.routesById[routeId] as AnyRoute
275
+ const remountFn =
276
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
277
+ router.options.defaultRemountDeps
278
+ const remountDeps = remountFn?.({
279
+ routeId,
280
+ loaderDeps: match.loaderDeps,
281
+ params: match._strictParams,
282
+ search: match._strictSearch,
283
+ })
284
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
285
+ const Comp = route.options.component ?? router.options.defaultComponent
286
+ const out = Comp ? <Comp key={key} /> : <Outlet />
287
+
288
+ if (match._displayPending) {
289
+ throw router.getMatch(match.id)?._nonReactive.displayPendingPromise
290
+ }
291
+
292
+ if (match._forcePending) {
293
+ throw router.getMatch(match.id)?._nonReactive.minPendingPromise
294
+ }
295
+
296
+ if (match.status === 'pending') {
297
+ throw router.getMatch(match.id)?._nonReactive.loadPromise
298
+ }
299
+
300
+ if (match.status === 'notFound') {
301
+ if (!isNotFound(match.error)) {
302
+ if (process.env.NODE_ENV !== 'production') {
303
+ throw new Error('Invariant failed: Expected a notFound error')
304
+ }
305
+
306
+ invariant()
307
+ }
308
+ return renderRouteNotFound(router, route, match.error)
309
+ }
310
+
311
+ if (match.status === 'redirected') {
312
+ if (!isRedirect(match.error)) {
313
+ if (process.env.NODE_ENV !== 'production') {
314
+ throw new Error('Invariant failed: Expected a redirect error')
315
+ }
316
+
317
+ invariant()
318
+ }
319
+ throw router.getMatch(match.id)?._nonReactive.loadPromise
320
+ }
321
+
322
+ if (match.status === 'error') {
323
+ const RouteErrorComponent =
324
+ (route.options.errorComponent ??
325
+ router.options.defaultErrorComponent) ||
326
+ ErrorComponent
327
+ return (
328
+ <RouteErrorComponent
329
+ error={match.error as any}
330
+ reset={undefined as any}
331
+ info={{
332
+ componentStack: '',
333
+ }}
334
+ />
335
+ )
336
+ }
337
+
338
+ return out
339
+ }
340
+
341
+ // eslint-disable-next-line react-hooks/rules-of-hooks
342
+ const matchStore = router.stores.activeMatchStoresById.get(matchId)
343
+ if (!matchStore) {
344
+ if (process.env.NODE_ENV !== 'production') {
345
+ throw new Error(
346
+ `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
347
+ )
348
+ }
216
349
 
350
+ invariant()
351
+ }
352
+ // eslint-disable-next-line react-hooks/rules-of-hooks
353
+ const match = useStore(matchStore, (value) => value)
354
+ const routeId = match.routeId as string
355
+ const route = router.routesById[routeId] as AnyRoute
356
+ // eslint-disable-next-line react-hooks/rules-of-hooks
357
+ const key = React.useMemo(() => {
358
+ const remountFn =
359
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
360
+ router.options.defaultRemountDeps
361
+ const remountDeps = remountFn?.({
362
+ routeId,
363
+ loaderDeps: match.loaderDeps,
364
+ params: match._strictParams,
365
+ search: match._strictSearch,
366
+ })
367
+ return remountDeps ? JSON.stringify(remountDeps) : undefined
368
+ }, [
369
+ routeId,
370
+ match.loaderDeps,
371
+ match._strictParams,
372
+ match._strictSearch,
373
+ router.options.defaultRemountDeps,
374
+ router.routesById,
375
+ ])
376
+
377
+ // eslint-disable-next-line react-hooks/rules-of-hooks
217
378
  const out = React.useMemo(() => {
218
379
  const Comp = route.options.component ?? router.options.defaultComponent
219
380
  if (Comp) {
@@ -256,14 +417,26 @@ export const MatchInner = React.memo(function MatchInnerImpl({
256
417
  }
257
418
 
258
419
  if (match.status === 'notFound') {
259
- invariant(isNotFound(match.error), 'Expected a notFound error')
420
+ if (!isNotFound(match.error)) {
421
+ if (process.env.NODE_ENV !== 'production') {
422
+ throw new Error('Invariant failed: Expected a notFound error')
423
+ }
424
+
425
+ invariant()
426
+ }
260
427
  return renderRouteNotFound(router, route, match.error)
261
428
  }
262
429
 
263
430
  if (match.status === 'redirected') {
264
431
  // Redirects should be handled by the router transition. If we happen to
265
432
  // encounter a redirect here, it's a bug. Let's warn, but render nothing.
266
- invariant(isRedirect(match.error), 'Expected a redirect error')
433
+ if (!isRedirect(match.error)) {
434
+ if (process.env.NODE_ENV !== 'production') {
435
+ throw new Error('Invariant failed: Expected a redirect error')
436
+ }
437
+
438
+ invariant()
439
+ }
267
440
 
268
441
  // warning(
269
442
  // false,
@@ -309,37 +482,57 @@ export const MatchInner = React.memo(function MatchInnerImpl({
309
482
  export const Outlet = React.memo(function OutletImpl() {
310
483
  const router = useRouter()
311
484
  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
- })
485
+
486
+ let routeId: string | undefined
487
+ let parentGlobalNotFound = false
488
+ let childMatchId: string | undefined
489
+
490
+ if (isServer ?? router.isServer) {
491
+ const matches = router.stores.activeMatchesSnapshot.state
492
+ const parentIndex = matchId
493
+ ? matches.findIndex((match) => match.id === matchId)
494
+ : -1
495
+ const parentMatch = parentIndex >= 0 ? matches[parentIndex] : undefined
496
+ routeId = parentMatch?.routeId as string | undefined
497
+ parentGlobalNotFound = parentMatch?.globalNotFound ?? false
498
+ childMatchId =
499
+ parentIndex >= 0 ? (matches[parentIndex + 1]?.id as string) : undefined
500
+ } else {
501
+ // Subscribe directly to the match store from the pool instead of
502
+ // the two-level byId → matchStore pattern.
503
+ const parentMatchStore = matchId
504
+ ? router.stores.activeMatchStoresById.get(matchId)
505
+ : undefined
506
+
507
+ // eslint-disable-next-line react-hooks/rules-of-hooks
508
+ ;[routeId, parentGlobalNotFound] = useStore(parentMatchStore, (match) => [
509
+ match?.routeId as string | undefined,
510
+ match?.globalNotFound ?? false,
511
+ ])
512
+
513
+ // eslint-disable-next-line react-hooks/rules-of-hooks
514
+ childMatchId = useStore(router.stores.matchesId, (ids) => {
515
+ const index = ids.findIndex((id) => id === matchId)
516
+ return ids[index + 1]
517
+ })
518
+ }
519
+
520
+ const route = routeId ? router.routesById[routeId] : undefined
337
521
 
338
522
  const pendingElement = router.options.defaultPendingComponent ? (
339
523
  <router.options.defaultPendingComponent />
340
524
  ) : null
341
525
 
342
526
  if (parentGlobalNotFound) {
527
+ if (!route) {
528
+ if (process.env.NODE_ENV !== 'production') {
529
+ throw new Error(
530
+ 'Invariant failed: Could not resolve route for Outlet render',
531
+ )
532
+ }
533
+
534
+ invariant()
535
+ }
343
536
  return renderRouteNotFound(router, route, undefined)
344
537
  }
345
538
 
package/src/Matches.tsx CHANGED
@@ -1,9 +1,8 @@
1
1
  import * as React from 'react'
2
- import warning from 'tiny-warning'
3
- import { rootRouteId } from '@tanstack/router-core'
2
+ import { useStore } from '@tanstack/react-store'
3
+ import { replaceEqualDeep, rootRouteId } from '@tanstack/router-core'
4
4
  import { isServer } from '@tanstack/router-core/isServer'
5
5
  import { CatchBoundary, ErrorComponent } from './CatchBoundary'
6
- import { useRouterState } from './useRouterState'
7
6
  import { useRouter } from './useRouter'
8
7
  import { Transitioner } from './Transitioner'
9
8
  import { matchContext } from './matchContext'
@@ -28,7 +27,6 @@ import type {
28
27
  ResolveRelativePath,
29
28
  ResolveRoute,
30
29
  RouteByPath,
31
- RouterState,
32
30
  ToSubOptionsProps,
33
31
  } from '@tanstack/router-core'
34
32
 
@@ -78,15 +76,15 @@ export function Matches() {
78
76
 
79
77
  function MatchesInner() {
80
78
  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
- })
79
+ const _isServer = isServer ?? router.isServer
80
+ const matchId = _isServer
81
+ ? router.stores.firstMatchId.state
82
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
83
+ useStore(router.stores.firstMatchId, (id) => id)
84
+ const resetKey = _isServer
85
+ ? router.stores.loadedAt.state
86
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
87
+ useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
90
88
 
91
89
  const matchComponent = matchId ? <Match matchId={matchId} /> : null
92
90
 
@@ -101,11 +99,10 @@ function MatchesInner() {
101
99
  onCatch={
102
100
  process.env.NODE_ENV !== 'production'
103
101
  ? (error) => {
104
- warning(
105
- false,
106
- `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
102
+ console.warn(
103
+ `Warning: The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
107
104
  )
108
- warning(false, error.message || error.toString())
105
+ console.warn(`Warning: ${error.message || error.toString()}`)
109
106
  }
110
107
  : undefined
111
108
  }
@@ -144,10 +141,10 @@ export type UseMatchRouteOptions<
144
141
  export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
145
142
  const router = useRouter()
146
143
 
147
- useRouterState({
148
- select: (s) => [s.location.href, s.resolvedLocation?.href, s.status],
149
- structuralSharing: true as any,
150
- })
144
+ if (!(isServer ?? router.isServer)) {
145
+ // eslint-disable-next-line react-hooks/rules-of-hooks
146
+ useStore(router.stores.matchRouteReactivity, (d) => d)
147
+ }
151
148
 
152
149
  return React.useCallback(
153
150
  <
@@ -238,15 +235,36 @@ export function useMatches<
238
235
  opts?: UseMatchesBaseOptions<TRouter, TSelected, TStructuralSharing> &
239
236
  StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
240
237
  ): 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>
238
+ const router = useRouter<TRouter>()
239
+ const previousResult =
240
+ React.useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(
241
+ undefined,
242
+ )
243
+
244
+ if (isServer ?? router.isServer) {
245
+ const matches = router.stores.activeMatchesSnapshot.state as Array<
246
+ MakeRouteMatchUnion<TRouter>
247
+ >
248
+ return (opts?.select ? opts.select(matches) : matches) as UseMatchesResult<
249
+ TRouter,
250
+ TSelected
251
+ >
252
+ }
253
+
254
+ // eslint-disable-next-line react-hooks/rules-of-hooks
255
+ return useStore(router.stores.activeMatchesSnapshot, (matches) => {
256
+ const selected = opts?.select
257
+ ? opts.select(matches as Array<MakeRouteMatchUnion<TRouter>>)
258
+ : (matches as any)
259
+
260
+ if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
261
+ const shared = replaceEqualDeep(previousResult.current, selected)
262
+ previousResult.current = shared
263
+ return shared
264
+ }
265
+
266
+ return selected
267
+ }) as UseMatchesResult<TRouter, TSelected>
250
268
  }
251
269
 
252
270
  /**