Skip to content

Commit 4b792b9

Browse files
fix(solid-router): RouteProvider Wrap and Match InnerWrap support propagating context to their children (#5033)
Refactored a bit router options.Wrap and options.InnerWrap usage in RouterProvider and Match components. Now they will be able to provide context for their children components. Added few tests for scenarios that were failing before. Fixes #3744 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * New Features * Consistent wrapper handling across routing so routed content and pending states share the same wrapper/context without adding extra DOM nodes. * Exposed createMemoryHistory for memory-based routing in apps and tests. * Refactor * Simplified suspense and wrapper handling for clearer, safer rendering behavior. * Tests * Added tests verifying context propagation through wrappers and into pending (fallback) states. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a98c89c commit 4b792b9

File tree

4 files changed

+163
-26
lines changed

4 files changed

+163
-26
lines changed

packages/solid-router/src/Matches.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,27 @@ declare module '@tanstack/router-core' {
3838
export function Matches() {
3939
const router = useRouter()
4040

41-
const pendingElement = router.options.defaultPendingComponent ? (
42-
<router.options.defaultPendingComponent />
43-
) : null
44-
4541
// Do not render a root Suspense during SSR or hydrating from SSR
4642
const ResolvedSuspense =
4743
router.isServer || (typeof document !== 'undefined' && router.ssr)
4844
? SafeFragment
4945
: Solid.Suspense
5046

51-
const inner = (
52-
<ResolvedSuspense fallback={pendingElement}>
53-
{!router.isServer && <Transitioner />}
54-
<MatchesInner />
55-
</ResolvedSuspense>
56-
)
47+
const OptionalWrapper = router.options.InnerWrap || SafeFragment
5748

58-
return router.options.InnerWrap ? (
59-
<router.options.InnerWrap>{inner}</router.options.InnerWrap>
60-
) : (
61-
inner
49+
return (
50+
<OptionalWrapper>
51+
<ResolvedSuspense
52+
fallback={
53+
router.options.defaultPendingComponent ? (
54+
<router.options.defaultPendingComponent />
55+
) : null
56+
}
57+
>
58+
{!router.isServer && <Transitioner />}
59+
<MatchesInner />
60+
</ResolvedSuspense>
61+
</OptionalWrapper>
6262
)
6363
}
6464

@@ -74,8 +74,10 @@ function MatchesInner() {
7474
select: (s) => s.loadedAt,
7575
})
7676

77-
const matchComponent = () =>
78-
matchId() ? <Match matchId={matchId()!} /> : null
77+
const matchComponent = () => {
78+
const id = matchId()
79+
return id ? <Match matchId={id} /> : null
80+
}
7981

8082
return (
8183
<matchContext.Provider value={matchId}>

packages/solid-router/src/RouterProvider.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Matches } from './Matches'
22
import { getRouterContext } from './routerContext'
3+
import { SafeFragment } from './SafeFragment'
34
import type * as Solid from 'solid-js'
45
import type {
56
AnyRouter,
@@ -29,17 +30,15 @@ export function RouterContextProvider<
2930

3031
const routerContext = getRouterContext()
3132

32-
const provider = (
33-
<routerContext.Provider value={router as AnyRouter}>
34-
{children()}
35-
</routerContext.Provider>
36-
)
37-
38-
if (router.options.Wrap) {
39-
return <router.options.Wrap>{provider}</router.options.Wrap>
40-
}
33+
const OptionalWrapper = router.options.Wrap || SafeFragment
4134

42-
return provider
35+
return (
36+
<OptionalWrapper>
37+
<routerContext.Provider value={router as AnyRouter}>
38+
{children()}
39+
</routerContext.Provider>
40+
</OptionalWrapper>
41+
)
4342
}
4443

4544
export function RouterProvider<

packages/solid-router/tests/Matches.test.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { expect, test } from 'vitest'
22
import { fireEvent, render, screen } from '@solidjs/testing-library'
3+
import { createContext, useContext } from 'solid-js'
34
import {
45
Link,
56
Outlet,
67
RouterProvider,
8+
createMemoryHistory,
79
createRootRoute,
810
createRoute,
911
createRouter,
1012
isMatch,
1113
useMatches,
1214
} from '../src'
15+
import { sleep } from './utils'
1316

1417
const rootRoute = createRootRoute()
1518

@@ -122,3 +125,99 @@ test('when filtering useMatches by loaderData', async () => {
122125

123126
expect(await screen.findByText('Incorrect Matches -')).toBeInTheDocument()
124127
})
128+
129+
test('Matches provides InnerWrap context to route components', async () => {
130+
const rootRoute = createRootRoute({
131+
component: () => {
132+
const contextValue = useContext(ctx)
133+
expect(contextValue, 'Context is not provided').not.toBeUndefined()
134+
135+
return <div>{contextValue}</div>
136+
},
137+
})
138+
139+
const routeTree = rootRoute.addChildren([])
140+
const router = createRouter({
141+
routeTree,
142+
})
143+
144+
const ctx = createContext<string>()
145+
146+
const app = render(() => (
147+
<RouterProvider
148+
router={router}
149+
InnerWrap={(props) => {
150+
return (
151+
<ctx.Provider value={'context-for-children'}>
152+
{props.children}
153+
</ctx.Provider>
154+
)
155+
}}
156+
/>
157+
))
158+
159+
const indexElem = await app.findByText('context-for-children')
160+
expect(indexElem).toBeInTheDocument()
161+
})
162+
163+
test('Matches provides InnerWrap context to defaultPendingComponent', async () => {
164+
const rootRoute = createRootRoute({})
165+
const indexRoute = createRoute({
166+
getParentRoute: () => rootRoute,
167+
path: '/',
168+
component: () => {
169+
return (
170+
<div>
171+
<Link to="/home">link to home</Link>
172+
</div>
173+
)
174+
},
175+
})
176+
177+
const homeRoute = createRoute({
178+
getParentRoute: () => rootRoute,
179+
path: '/home',
180+
loader: () => sleep(300),
181+
component: () => <div>Home page</div>,
182+
})
183+
184+
const routeTree = rootRoute.addChildren([homeRoute, indexRoute])
185+
const router = createRouter({
186+
routeTree,
187+
history: createMemoryHistory({
188+
initialEntries: ['/'],
189+
}),
190+
})
191+
192+
const ctx = createContext<string>()
193+
194+
const app = render(() => (
195+
<RouterProvider
196+
router={router}
197+
defaultPendingMs={200}
198+
defaultPendingComponent={() => {
199+
const contextValue = useContext(ctx)
200+
expect(contextValue, 'Context is not provided').not.toBeUndefined()
201+
202+
return <div>{contextValue}</div>
203+
}}
204+
InnerWrap={(props) => {
205+
return (
206+
<ctx.Provider value={'context-for-default-pending'}>
207+
{props.children}
208+
</ctx.Provider>
209+
)
210+
}}
211+
/>
212+
))
213+
214+
const linkToHome = await app.findByRole('link', {
215+
name: 'link to home',
216+
})
217+
expect(linkToHome).toBeInTheDocument()
218+
219+
fireEvent.click(linkToHome)
220+
221+
const indexElem = await app.findByText('context-for-default-pending')
222+
expect(indexElem).toBeInTheDocument()
223+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { render } from '@solidjs/testing-library'
3+
import { createContext, useContext } from 'solid-js'
4+
import { createRootRoute, createRouter } from '../src'
5+
import { RouterProvider } from '../src/RouterProvider'
6+
7+
describe('RouterProvider', () => {
8+
it('should provide context through RouterProvider Wrap', async () => {
9+
const ctx = createContext<string>()
10+
11+
const rootRoute = createRootRoute({
12+
component: () => {
13+
const contextValue = useContext(ctx)
14+
expect(contextValue, 'Context is not provided').not.toBeUndefined()
15+
16+
return <div>{contextValue}</div>
17+
},
18+
})
19+
20+
const routeTree = rootRoute.addChildren([])
21+
const router = createRouter({
22+
routeTree,
23+
})
24+
25+
const app = render(() => (
26+
<RouterProvider
27+
router={router}
28+
Wrap={(props) => {
29+
return <ctx.Provider value={'findMe'}>{props.children}</ctx.Provider>
30+
}}
31+
/>
32+
))
33+
34+
const indexElem = await app.findByText('findMe')
35+
expect(indexElem).toBeInTheDocument()
36+
})
37+
})

0 commit comments

Comments
 (0)