fix(solid-router): wrap root Outlet child with CatchBoundary#6847
fix(solid-router): wrap root Outlet child with CatchBoundary#6847ljho01 wants to merge 1 commit intoTanStack:mainfrom
Conversation
When `shellComponent` renders `<Outlet />` directly instead of
`{props.children}`, the root's CatchBoundary (from Match) is never
placed in the render tree. Errors re-thrown from a child route's
errorComponent have no parent boundary to propagate to.
Fix: unconditionally wrap the root Outlet's child `<Match>` with a
CatchBoundary using the root's errorComponent. The extra boundary is
harmless when shellComponent does render children — the inner
CatchBoundary (from Match) catches first.
Closes TanStack#6845
Made-with: Cursor
📝 WalkthroughWalkthroughModifies the Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bundle Size Benchmarks
Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/solid-router/src/Match.tsx`:
- Around line 391-394: The root Match boundary changes must mirror the full root
behavior: update the Match component logic that defines rootErrorComponent and
rootResetKey so its onCatch handler forwards to the root/default onCatch (call
or delegate to router.options.onCatch or the root route's onCatch) instead of
swallowing errors, ensure isNotFound(error) results in rethrow only when a
sibling root CatchNotFound is present otherwise forward to the root notFound
handler (router.options.notFoundComponent or route-level notFoundComponent), and
in the direct-Outlet shell path ensure ordinary errors still bubble to the root
onCatch and descendant errorComponents that throw notFound(...) are captured by
a sibling root CatchNotFound equivalent; locate and modify the onCatch/code
paths and the error escalation flow near rootErrorComponent/rootResetKey and the
Outlet-shell branch to delegate to the router's root/default handlers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 64dc9398-72ac-45b8-8c91-2580580f9124
📒 Files selected for processing (1)
packages/solid-router/src/Match.tsx
| const rootErrorComponent = () => | ||
| router.routesById[rootRouteId]?.options.errorComponent ?? | ||
| router.options.defaultErrorComponent | ||
| const rootResetKey = useRouterState({ select: (s) => s.loadedAt }) |
There was a problem hiding this comment.
Mirror the full root Match boundary behavior here.
This restores the root errorComponent, but it still diverges from the normal root path at Lines 102-143: onCatch no longer forwards to the root/default onCatch, and isNotFound(error) is rethrown without a sibling root CatchNotFound. In the direct-<Outlet /> shell case, ordinary errors will skip root onCatch, and a descendant errorComponent that escalates with throw notFound(...) can still bypass the root notFoundComponent.
Proposed parity fix
const rootErrorComponent = () =>
router.routesById[rootRouteId]?.options.errorComponent ??
router.options.defaultErrorComponent
+ const rootOnCatch = () =>
+ router.routesById[rootRouteId]?.options.onCatch ??
+ router.options.defaultOnCatch
+ const rootNotFoundComponent = () =>
+ router.routesById[rootRouteId]?.options.notFoundComponent ??
+ router.options.notFoundRoute?.options.component
const rootResetKey = useRouterState({ select: (s) => s.loadedAt })
return (
@@
<Solid.Suspense
fallback={
<Dynamic component={router.options.defaultPendingComponent} />
}
>
- {rootErrorComponent() ? (
- <CatchBoundary
- getResetKey={() => rootResetKey()}
- errorComponent={rootErrorComponent()!}
- onCatch={(error) => {
- if (isNotFound(error)) throw error
- }}
- >
- {childMatch()}
- </CatchBoundary>
- ) : (
- childMatch()
- )}
+ <Dynamic
+ component={rootErrorComponent() ? CatchBoundary : SafeFragment}
+ getResetKey={() => rootResetKey()}
+ errorComponent={rootErrorComponent() || ErrorComponent}
+ onCatch={(error: Error) => {
+ if (isNotFound(error)) throw error
+ warning(false, `Error in route match: ${rootRouteId}`)
+ rootOnCatch()?.(error)
+ }}
+ >
+ <Dynamic
+ component={
+ rootNotFoundComponent() ? CatchNotFound : SafeFragment
+ }
+ fallback={(error: any) => {
+ if (
+ !rootNotFoundComponent() ||
+ (error.routeId && error.routeId !== rootRouteId)
+ ) {
+ throw error
+ }
+
+ return (
+ <Dynamic
+ component={rootNotFoundComponent()}
+ {...error}
+ />
+ )
+ }}
+ >
+ {childMatch()}
+ </Dynamic>
+ </Dynamic>
</Solid.Suspense>Also applies to: 420-432
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/solid-router/src/Match.tsx` around lines 391 - 394, The root Match
boundary changes must mirror the full root behavior: update the Match component
logic that defines rootErrorComponent and rootResetKey so its onCatch handler
forwards to the root/default onCatch (call or delegate to router.options.onCatch
or the root route's onCatch) instead of swallowing errors, ensure
isNotFound(error) results in rethrow only when a sibling root CatchNotFound is
present otherwise forward to the root notFound handler
(router.options.notFoundComponent or route-level notFoundComponent), and in the
direct-Outlet shell path ensure ordinary errors still bubble to the root onCatch
and descendant errorComponents that throw notFound(...) are captured by a
sibling root CatchNotFound equivalent; locate and modify the onCatch/code paths
and the error escalation flow near rootErrorComponent/rootResetKey and the
Outlet-shell branch to delegate to the router's root/default handlers.
|
this needs tests. preferably unit tests, otherwise e2e tests |
When
shellComponentrenders<Outlet />directly (the natural pattern) instead of{props.children}, the root'sCatchBoundaryfromMatchis never placed in the render tree. Errors re-thrown from a child route'serrorComponenthave no parent boundary to propagate to — the page goes blank.Fix
Unconditionally wrap the root Outlet's child
<Match>with aCatchBoundaryusing the root'serrorComponent. The extra boundary is harmless whenshellComponentdoes render{props.children}— the innerCatchBoundary(fromMatch) catches first.1 file changed, +21 −3 —
packages/solid-router/src/Match.tsxVerification
pnpm vitest run)Closes #6845
Made with Cursor
Summary by CodeRabbit