@tanstack/solid-router 1.134.11 → 1.134.12

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/Match.tsx CHANGED
@@ -25,10 +25,12 @@ export const Match = (props: { matchId: string }) => {
25
25
  select: (s) => {
26
26
  const match = s.matches.find((d) => d.id === props.matchId)
27
27
 
28
- invariant(
29
- match,
30
- `Could not find match for matchId "${props.matchId}". Please file an issue!`,
31
- )
28
+ // During navigation transitions, matches can be temporarily removed
29
+ // Return null to avoid errors - the component will handle this gracefully
30
+ if (!match) {
31
+ return null
32
+ }
33
+
32
34
  return {
33
35
  routeId: match.routeId,
34
36
  ssr: match.ssr,
@@ -37,9 +39,12 @@ export const Match = (props: { matchId: string }) => {
37
39
  },
38
40
  })
39
41
 
40
- const route: () => AnyRoute = () => router.routesById[matchState().routeId]
42
+ // If match doesn't exist yet, return null (component is being unmounted or not ready)
43
+ if (!matchState()) return null
41
44
 
42
- const PendingComponent = () =>
45
+ const route: () => AnyRoute = () => router.routesById[matchState()!.routeId]
46
+
47
+ const resolvePendingComponent = () =>
43
48
  route().options.pendingComponent ?? router.options.defaultPendingComponent
44
49
 
45
50
  const routeErrorComponent = () =>
@@ -56,19 +61,9 @@ export const Match = (props: { matchId: string }) => {
56
61
  : route().options.notFoundComponent
57
62
 
58
63
  const resolvedNoSsr =
59
- matchState().ssr === false || matchState().ssr === 'data-only'
60
-
61
- const ResolvedSuspenseBoundary = () =>
62
- // If we're on the root route, allow forcefully wrapping in suspense
63
- (!route().isRoot ||
64
- route().options.wrapInSuspense ||
65
- resolvedNoSsr ||
66
- matchState()._displayPending) &&
67
- (route().options.wrapInSuspense ??
68
- PendingComponent() ??
69
- ((route().options.errorComponent as any)?.preload || resolvedNoSsr))
70
- ? Solid.Suspense
71
- : SafeFragment
64
+ matchState()!.ssr === false || matchState()!.ssr === 'data-only'
65
+
66
+ const ResolvedSuspenseBoundary = () => Solid.Suspense
72
67
 
73
68
  const ResolvedCatchBoundary = () =>
74
69
  routeErrorComponent() ? CatchBoundary : SafeFragment
@@ -99,7 +94,7 @@ export const Match = (props: { matchId: string }) => {
99
94
  fallback={
100
95
  // Don't show fallback on server when using no-ssr mode to avoid hydration mismatch
101
96
  router.isServer || resolvedNoSsr ? undefined : (
102
- <Dynamic component={PendingComponent()} />
97
+ <Dynamic component={resolvePendingComponent()} />
103
98
  )
104
99
  }
105
100
  >
@@ -121,7 +116,7 @@ export const Match = (props: { matchId: string }) => {
121
116
  // route ID which doesn't match the current route, rethrow the error
122
117
  if (
123
118
  !routeNotFoundComponent() ||
124
- (error.routeId && error.routeId !== matchState().routeId) ||
119
+ (error.routeId && error.routeId !== matchState()!.routeId) ||
125
120
  (!error.routeId && !route().isRoot)
126
121
  )
127
122
  throw error
@@ -135,7 +130,7 @@ export const Match = (props: { matchId: string }) => {
135
130
  <Solid.Match when={resolvedNoSsr}>
136
131
  <Solid.Show
137
132
  when={!router.isServer}
138
- fallback={<Dynamic component={PendingComponent()} />}
133
+ fallback={<Dynamic component={resolvePendingComponent()} />}
139
134
  >
140
135
  <MatchInner matchId={props.matchId} />
141
136
  </Solid.Show>
@@ -190,7 +185,13 @@ export const MatchInner = (props: { matchId: string }): any => {
190
185
 
191
186
  const matchState = useRouterState({
192
187
  select: (s) => {
193
- const match = s.matches.find((d) => d.id === props.matchId)!
188
+ const match = s.matches.find((d) => d.id === props.matchId)
189
+
190
+ // During navigation transitions, matches can be temporarily removed
191
+ if (!match) {
192
+ return null
193
+ }
194
+
194
195
  const routeId = match.routeId as string
195
196
 
196
197
  const remountFn =
@@ -218,11 +219,13 @@ export const MatchInner = (props: { matchId: string }): any => {
218
219
  },
219
220
  })
220
221
 
221
- const route = () => router.routesById[matchState().routeId]!
222
+ if (!matchState()) return null
223
+
224
+ const route = () => router.routesById[matchState()!.routeId]!
222
225
 
223
- const match = () => matchState().match
226
+ const match = () => matchState()!.match
224
227
 
225
- const componentKey = () => matchState().key ?? matchState().match.id
228
+ const componentKey = () => matchState()!.key ?? matchState()!.match.id
226
229
 
227
230
  const out = () => {
228
231
  const Comp = route().options.component ?? router.options.defaultComponent
@@ -287,7 +290,18 @@ export const MatchInner = (props: { matchId: string }): any => {
287
290
  return router.getMatch(match().id)?._nonReactive.loadPromise
288
291
  })
289
292
 
290
- return <>{loaderResult()}</>
293
+ const FallbackComponent =
294
+ route().options.pendingComponent ??
295
+ router.options.defaultPendingComponent
296
+
297
+ return (
298
+ <>
299
+ {FallbackComponent ? (
300
+ <Dynamic component={FallbackComponent} />
301
+ ) : null}
302
+ {loaderResult()}
303
+ </>
304
+ )
291
305
  }}
292
306
  </Solid.Match>
293
307
  <Solid.Match when={match().status === 'notFound'}>
@@ -350,10 +364,13 @@ export const Outlet = () => {
350
364
  select: (s) => {
351
365
  const matches = s.matches
352
366
  const parentMatch = matches.find((d) => d.id === matchId())
353
- invariant(
354
- parentMatch,
355
- `Could not find parent match for matchId "${matchId()}"`,
356
- )
367
+
368
+ // During navigation transitions, parent match can be temporarily removed
369
+ // Return false to avoid errors - the component will handle this gracefully
370
+ if (!parentMatch) {
371
+ return false
372
+ }
373
+
357
374
  return parentMatch.globalNotFound
358
375
  },
359
376
  })
@@ -388,20 +405,21 @@ export const Outlet = () => {
388
405
  </Solid.Show>
389
406
  }
390
407
  >
391
- {(matchId) => {
392
- // const nextMatch = <Match matchId={matchId()} />
408
+ {(matchIdAccessor) => {
409
+ // Use a memo to avoid stale accessor errors while keeping reactivity
410
+ const currentMatchId = Solid.createMemo(() => matchIdAccessor())
393
411
 
394
412
  return (
395
413
  <Solid.Show
396
- when={matchId() === rootRouteId}
397
- fallback={<Match matchId={matchId()} />}
414
+ when={currentMatchId() === rootRouteId}
415
+ fallback={<Match matchId={currentMatchId()} />}
398
416
  >
399
417
  <Solid.Suspense
400
418
  fallback={
401
419
  <Dynamic component={router.options.defaultPendingComponent} />
402
420
  }
403
421
  >
404
- <Match matchId={matchId()} />
422
+ <Match matchId={currentMatchId()} />
405
423
  </Solid.Suspense>
406
424
  </Solid.Show>
407
425
  )
@@ -20,6 +20,7 @@ export function Transitioner() {
20
20
  }
21
21
 
22
22
  const [isTransitioning, setIsTransitioning] = Solid.createSignal(false)
23
+
23
24
  // Track pending state changes
24
25
  const hasPendingMatches = useRouterState({
25
26
  select: (s) => s.matches.some((d) => d.status === 'pending'),
@@ -34,10 +35,15 @@ export function Transitioner() {
34
35
  const isPagePending = () => isLoading() || hasPendingMatches()
35
36
  const previousIsPagePending = usePrevious(isPagePending)
36
37
 
37
- router.startTransition = async (fn: () => void | Promise<void>) => {
38
+ router.startTransition = (fn: () => void | Promise<void>) => {
38
39
  setIsTransitioning(true)
39
- await fn()
40
- setIsTransitioning(false)
40
+ Solid.startTransition(async () => {
41
+ try {
42
+ await fn()
43
+ } finally {
44
+ setIsTransitioning(false)
45
+ }
46
+ })
41
47
  }
42
48
 
43
49
  // Subscribe to location changes
package/src/useMatch.tsx CHANGED
@@ -73,24 +73,48 @@ export function useMatch<
73
73
  opts.from ? dummyMatchContext : matchContext,
74
74
  )
75
75
 
76
- const matchSelection = useRouterState({
76
+ // Create a signal to track error state separately from the match
77
+ const matchState: Solid.Accessor<{
78
+ match: any
79
+ shouldThrowError: boolean
80
+ }> = useRouterState({
77
81
  select: (state: any) => {
78
82
  const match = state.matches.find((d: any) =>
79
83
  opts.from ? opts.from === d.routeId : d.id === nearestMatchId(),
80
84
  )
81
85
 
82
- invariant(
83
- !((opts.shouldThrow ?? true) && !match),
84
- `Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`,
85
- )
86
-
87
86
  if (match === undefined) {
88
- return undefined
87
+ // During navigation transitions, check if the match exists in pendingMatches
88
+ const pendingMatch = state.pendingMatches?.find((d: any) =>
89
+ opts.from ? opts.from === d.routeId : d.id === nearestMatchId(),
90
+ )
91
+
92
+ // Determine if we should throw an error
93
+ const shouldThrowError =
94
+ !pendingMatch && !state.isTransitioning && (opts.shouldThrow ?? true)
95
+
96
+ return { match: undefined, shouldThrowError }
89
97
  }
90
98
 
91
- return opts.select ? opts.select(match) : match
99
+ return {
100
+ match: opts.select ? opts.select(match) : match,
101
+ shouldThrowError: false,
102
+ }
92
103
  },
93
104
  } as any)
94
105
 
95
- return matchSelection as any
106
+ // Use createEffect to throw errors outside the reactive selector context
107
+ // This allows error boundaries to properly catch the errors
108
+ Solid.createEffect(() => {
109
+ const state = matchState()
110
+ if (state.shouldThrowError) {
111
+ invariant(
112
+ false,
113
+ `Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`,
114
+ )
115
+ }
116
+ })
117
+
118
+ // Return an accessor that extracts just the match value
119
+ return Solid.createMemo(() => matchState().match) as any
96
120
  }