Skip to content

Commit 22dc152

Browse files
authored
fix(solid-router): use keyed Show in Outlet to fix child route rendering with useQuery (#7204)
1 parent 3d4b15b commit 22dc152

15 files changed

Lines changed: 360 additions & 9 deletions

File tree

.changeset/wild-lies-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/solid-router': patch
3+
---
4+
5+
fix: use keyed Show in Outlet to fix child route rendering with useQuery
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "tanstack-solid-start-e2e-solid-query-layout-suspense",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite dev --port 3000",
7+
"dev:e2e": "vite dev --port $PORT",
8+
"build": "vite build && tsc --noEmit",
9+
"start": "node .output/server/index.mjs",
10+
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
11+
},
12+
"dependencies": {
13+
"@tanstack/solid-query": "^5.90.9",
14+
"@tanstack/solid-router": "workspace:^",
15+
"@tanstack/solid-start": "workspace:^",
16+
"solid-js": "^1.9.10",
17+
"vite": "^8.0.0"
18+
},
19+
"devDependencies": {
20+
"@playwright/test": "^1.50.1",
21+
"@tanstack/router-e2e-utils": "workspace:^",
22+
"@types/node": "^22.10.2",
23+
"typescript": "^6.0.2",
24+
"vite-plugin-solid": "^2.11.11"
25+
}
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineConfig, devices } from '@playwright/test'
2+
import { getTestServerPort } from '@tanstack/router-e2e-utils'
3+
import packageJson from './package.json' with { type: 'json' }
4+
5+
const PORT = await getTestServerPort(packageJson.name)
6+
const baseURL = `http://localhost:${PORT}`
7+
8+
export default defineConfig({
9+
testDir: './tests',
10+
workers: 1,
11+
reporter: [['line']],
12+
use: { baseURL },
13+
webServer: {
14+
command: `PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm dev:e2e`,
15+
url: baseURL,
16+
reuseExistingServer: !process.env.CI,
17+
stdout: 'pipe',
18+
},
19+
projects: [
20+
{
21+
name: 'chromium',
22+
use: { ...devices['Desktop Chrome'] },
23+
},
24+
],
25+
})
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/* eslint-disable */
2+
3+
// @ts-nocheck
4+
5+
// noinspection JSUnusedGlobalSymbols
6+
7+
// This file was automatically generated by TanStack Router.
8+
// You should NOT make any changes in this file as it will be overwritten.
9+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10+
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as LayoutRouteImport } from './routes/layout'
13+
import { Route as IndexRouteImport } from './routes/index'
14+
import { Route as LayoutPage2RouteImport } from './routes/layout.page2'
15+
16+
const LayoutRoute = LayoutRouteImport.update({
17+
id: '/layout',
18+
path: '/layout',
19+
getParentRoute: () => rootRouteImport,
20+
} as any)
21+
const IndexRoute = IndexRouteImport.update({
22+
id: '/',
23+
path: '/',
24+
getParentRoute: () => rootRouteImport,
25+
} as any)
26+
const LayoutPage2Route = LayoutPage2RouteImport.update({
27+
id: '/page2',
28+
path: '/page2',
29+
getParentRoute: () => LayoutRoute,
30+
} as any)
31+
32+
export interface FileRoutesByFullPath {
33+
'/': typeof IndexRoute
34+
'/layout': typeof LayoutRouteWithChildren
35+
'/layout/page2': typeof LayoutPage2Route
36+
}
37+
export interface FileRoutesByTo {
38+
'/': typeof IndexRoute
39+
'/layout': typeof LayoutRouteWithChildren
40+
'/layout/page2': typeof LayoutPage2Route
41+
}
42+
export interface FileRoutesById {
43+
__root__: typeof rootRouteImport
44+
'/': typeof IndexRoute
45+
'/layout': typeof LayoutRouteWithChildren
46+
'/layout/page2': typeof LayoutPage2Route
47+
}
48+
export interface FileRouteTypes {
49+
fileRoutesByFullPath: FileRoutesByFullPath
50+
fullPaths: '/' | '/layout' | '/layout/page2'
51+
fileRoutesByTo: FileRoutesByTo
52+
to: '/' | '/layout' | '/layout/page2'
53+
id: '__root__' | '/' | '/layout' | '/layout/page2'
54+
fileRoutesById: FileRoutesById
55+
}
56+
export interface RootRouteChildren {
57+
IndexRoute: typeof IndexRoute
58+
LayoutRoute: typeof LayoutRouteWithChildren
59+
}
60+
61+
declare module '@tanstack/solid-router' {
62+
interface FileRoutesByPath {
63+
'/layout': {
64+
id: '/layout'
65+
path: '/layout'
66+
fullPath: '/layout'
67+
preLoaderRoute: typeof LayoutRouteImport
68+
parentRoute: typeof rootRouteImport
69+
}
70+
'/': {
71+
id: '/'
72+
path: '/'
73+
fullPath: '/'
74+
preLoaderRoute: typeof IndexRouteImport
75+
parentRoute: typeof rootRouteImport
76+
}
77+
'/layout/page2': {
78+
id: '/layout/page2'
79+
path: '/page2'
80+
fullPath: '/layout/page2'
81+
preLoaderRoute: typeof LayoutPage2RouteImport
82+
parentRoute: typeof LayoutRoute
83+
}
84+
}
85+
}
86+
87+
interface LayoutRouteChildren {
88+
LayoutPage2Route: typeof LayoutPage2Route
89+
}
90+
91+
const LayoutRouteChildren: LayoutRouteChildren = {
92+
LayoutPage2Route: LayoutPage2Route,
93+
}
94+
95+
const LayoutRouteWithChildren =
96+
LayoutRoute._addFileChildren(LayoutRouteChildren)
97+
98+
const rootRouteChildren: RootRouteChildren = {
99+
IndexRoute: IndexRoute,
100+
LayoutRoute: LayoutRouteWithChildren,
101+
}
102+
export const routeTree = rootRouteImport
103+
._addFileChildren(rootRouteChildren)
104+
._addFileTypes<FileRouteTypes>()
105+
106+
import type { getRouter } from './router.tsx'
107+
import type { createStart } from '@tanstack/solid-start'
108+
declare module '@tanstack/solid-start' {
109+
interface Register {
110+
ssr: true
111+
router: Awaited<ReturnType<typeof getRouter>>
112+
}
113+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
2+
import { createRouter } from '@tanstack/solid-router'
3+
import { routeTree } from './routeTree.gen'
4+
5+
export function getRouter() {
6+
const queryClient = new QueryClient()
7+
return createRouter({
8+
routeTree,
9+
Wrap: (props) => (
10+
<QueryClientProvider client={queryClient}>
11+
{props.children}
12+
</QueryClientProvider>
13+
),
14+
})
15+
}
16+
17+
declare module '@tanstack/solid-router' {
18+
interface Register {
19+
router: ReturnType<typeof getRouter>
20+
}
21+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
HeadContent,
3+
Link,
4+
Outlet,
5+
Scripts,
6+
createRootRouteWithContext,
7+
} from '@tanstack/solid-router'
8+
import { Suspense } from 'solid-js'
9+
import { HydrationScript } from 'solid-js/web'
10+
11+
export const Route = createRootRouteWithContext()({
12+
head: () => ({ meta: [{ charset: 'utf-8' }] }),
13+
shellComponent: RootDocument,
14+
})
15+
16+
function RootDocument() {
17+
return (
18+
<html>
19+
<head>
20+
<HydrationScript />
21+
</head>
22+
<body>
23+
<HeadContent />
24+
<nav>
25+
<Link to="/">Home</Link> <Link to="/layout/page2">👉 To page2</Link>
26+
</nav>
27+
<Suspense>
28+
<Outlet />
29+
</Suspense>
30+
<Scripts />
31+
</body>
32+
</html>
33+
)
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createFileRoute } from '@tanstack/solid-router'
2+
3+
export const Route = createFileRoute('/')({
4+
component: () => <main>Home page</main>,
5+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useQuery } from '@tanstack/solid-query'
2+
import { createFileRoute } from '@tanstack/solid-router'
3+
4+
export const Route = createFileRoute('/layout/page2')({
5+
component: RouteComponent,
6+
})
7+
8+
function RouteComponent() {
9+
const query = useQuery(() => ({
10+
queryKey: ['page2'],
11+
queryFn: async () => ({ data: 'page2-data' }),
12+
}))
13+
14+
return (
15+
<div data-testid="page2-content">
16+
Page data: {JSON.stringify(query.data)}
17+
</div>
18+
)
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useQuery } from '@tanstack/solid-query'
2+
import { Outlet, createFileRoute } from '@tanstack/solid-router'
3+
4+
export const Route = createFileRoute('/layout')({
5+
component: RouteComponent,
6+
})
7+
8+
function RouteComponent() {
9+
const query = useQuery(() => ({
10+
queryKey: ['layout'],
11+
queryFn: async () => {
12+
await new Promise((r) => setTimeout(r, 200))
13+
return { data: 'layout-data' }
14+
},
15+
}))
16+
17+
return (
18+
<div>
19+
<pre data-testid="layout-content">
20+
Layout data: {JSON.stringify(query.data?.data)}
21+
</pre>
22+
<Outlet />
23+
</div>
24+
)
25+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
// Reproduces https://github.com/TanStack/router/issues/7195
4+
test('layout with useQuery renders child after client navigation', async ({
5+
page,
6+
}) => {
7+
await page.goto('/')
8+
await expect(page.getByText('👉 To page2')).toBeVisible()
9+
await page.waitForLoadState('networkidle')
10+
11+
await page.getByText('👉 To page2').click()
12+
13+
await expect(page.getByTestId('page2-content')).toBeVisible({
14+
timeout: 5_000,
15+
})
16+
await expect(page.getByTestId('layout-content')).toBeVisible()
17+
})

0 commit comments

Comments
 (0)