Skip to content

Commit ad66e89

Browse files
fix: undefined / null serialization (#5501)
1 parent 60ff0e4 commit ad66e89

File tree

8 files changed

+234
-35
lines changed

8 files changed

+234
-35
lines changed

e2e/react-start/server-functions/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
1212
},
1313
"dependencies": {
14+
"@tanstack/react-query": "^5.66.0",
1415
"@tanstack/react-router": "workspace:^",
16+
"@tanstack/react-router-ssr-query": "workspace:^",
1517
"@tanstack/react-router-devtools": "workspace:^",
1618
"@tanstack/react-start": "workspace:^",
1719
"js-cookie": "^3.0.5",

e2e/react-start/server-functions/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserv
2222
import { Route as ConsistentRouteImport } from './routes/consistent'
2323
import { Route as AbortSignalRouteImport } from './routes/abort-signal'
2424
import { Route as IndexRouteImport } from './routes/index'
25+
import { Route as PrimitivesIndexRouteImport } from './routes/primitives/index'
2526
import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index'
2627
import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index'
2728
import { Route as FactoryIndexRouteImport } from './routes/factory/index'
@@ -97,6 +98,11 @@ const IndexRoute = IndexRouteImport.update({
9798
path: '/',
9899
getParentRoute: () => rootRouteImport,
99100
} as any)
101+
const PrimitivesIndexRoute = PrimitivesIndexRouteImport.update({
102+
id: '/primitives/',
103+
path: '/primitives/',
104+
getParentRoute: () => rootRouteImport,
105+
} as any)
100106
const MiddlewareIndexRoute = MiddlewareIndexRouteImport.update({
101107
id: '/middleware/',
102108
path: '/middleware/',
@@ -168,6 +174,7 @@ export interface FileRoutesByFullPath {
168174
'/factory': typeof FactoryIndexRoute
169175
'/formdata-redirect': typeof FormdataRedirectIndexRoute
170176
'/middleware': typeof MiddlewareIndexRoute
177+
'/primitives': typeof PrimitivesIndexRoute
171178
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
172179
}
173180
export interface FileRoutesByTo {
@@ -192,6 +199,7 @@ export interface FileRoutesByTo {
192199
'/factory': typeof FactoryIndexRoute
193200
'/formdata-redirect': typeof FormdataRedirectIndexRoute
194201
'/middleware': typeof MiddlewareIndexRoute
202+
'/primitives': typeof PrimitivesIndexRoute
195203
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
196204
}
197205
export interface FileRoutesById {
@@ -217,6 +225,7 @@ export interface FileRoutesById {
217225
'/factory/': typeof FactoryIndexRoute
218226
'/formdata-redirect/': typeof FormdataRedirectIndexRoute
219227
'/middleware/': typeof MiddlewareIndexRoute
228+
'/primitives/': typeof PrimitivesIndexRoute
220229
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
221230
}
222231
export interface FileRouteTypes {
@@ -243,6 +252,7 @@ export interface FileRouteTypes {
243252
| '/factory'
244253
| '/formdata-redirect'
245254
| '/middleware'
255+
| '/primitives'
246256
| '/formdata-redirect/target/$name'
247257
fileRoutesByTo: FileRoutesByTo
248258
to:
@@ -267,6 +277,7 @@ export interface FileRouteTypes {
267277
| '/factory'
268278
| '/formdata-redirect'
269279
| '/middleware'
280+
| '/primitives'
270281
| '/formdata-redirect/target/$name'
271282
id:
272283
| '__root__'
@@ -291,6 +302,7 @@ export interface FileRouteTypes {
291302
| '/factory/'
292303
| '/formdata-redirect/'
293304
| '/middleware/'
305+
| '/primitives/'
294306
| '/formdata-redirect/target/$name'
295307
fileRoutesById: FileRoutesById
296308
}
@@ -316,6 +328,7 @@ export interface RootRouteChildren {
316328
FactoryIndexRoute: typeof FactoryIndexRoute
317329
FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
318330
MiddlewareIndexRoute: typeof MiddlewareIndexRoute
331+
PrimitivesIndexRoute: typeof PrimitivesIndexRoute
319332
FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute
320333
}
321334

@@ -412,6 +425,13 @@ declare module '@tanstack/react-router' {
412425
preLoaderRoute: typeof IndexRouteImport
413426
parentRoute: typeof rootRouteImport
414427
}
428+
'/primitives/': {
429+
id: '/primitives/'
430+
path: '/primitives'
431+
fullPath: '/primitives'
432+
preLoaderRoute: typeof PrimitivesIndexRouteImport
433+
parentRoute: typeof rootRouteImport
434+
}
415435
'/middleware/': {
416436
id: '/middleware/'
417437
path: '/middleware'
@@ -500,6 +520,7 @@ const rootRouteChildren: RootRouteChildren = {
500520
FactoryIndexRoute: FactoryIndexRoute,
501521
FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
502522
MiddlewareIndexRoute: MiddlewareIndexRoute,
523+
PrimitivesIndexRoute: PrimitivesIndexRoute,
503524
FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute,
504525
}
505526
export const routeTree = rootRouteImport

e2e/react-start/server-functions/src/router.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { createRouter } from '@tanstack/react-router'
22
import { routeTree } from './routeTree.gen'
33
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
44
import { NotFound } from './components/NotFound'
5+
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
6+
import { QueryClient } from '@tanstack/react-query'
57

68
export function getRouter() {
9+
const queryClient = new QueryClient()
710
const router = createRouter({
811
routeTree,
912
defaultPreload: 'intent',
@@ -16,6 +19,7 @@ export function getRouter() {
1619
},
1720
},
1821
})
22+
setupRouterSsrQueryIntegration({ router, queryClient })
1923

2024
return router
2125
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { createFileRoute } from '@tanstack/react-router'
3+
import { createServerFn } from '@tanstack/react-start'
4+
import { useCallback } from 'react'
5+
import { z } from 'zod'
6+
export const Route = createFileRoute('/primitives/')({
7+
component: RouteComponent,
8+
})
9+
10+
function stringify(data: any) {
11+
return JSON.stringify(data === undefined ? '$undefined' : data)
12+
}
13+
14+
const $stringPost = createServerFn({ method: 'POST' })
15+
.inputValidator(z.string())
16+
.handler((ctx) => ctx.data)
17+
18+
const $stringGet = createServerFn({ method: 'GET' })
19+
.inputValidator(z.string())
20+
.handler((ctx) => ctx.data)
21+
22+
const $undefinedPost = createServerFn({ method: 'POST' })
23+
.inputValidator(z.undefined())
24+
.handler((ctx) => ctx.data)
25+
26+
const $undefinedGet = createServerFn({ method: 'GET' })
27+
.inputValidator(z.undefined())
28+
.handler((ctx) => ctx.data)
29+
30+
const $nullPost = createServerFn({ method: 'POST' })
31+
.inputValidator(z.null())
32+
.handler((ctx) => ctx.data)
33+
34+
const $nullGet = createServerFn({ method: 'GET' })
35+
.inputValidator(z.null())
36+
.handler((ctx) => ctx.data)
37+
38+
interface PrimitiveComponentProps<T> {
39+
serverFn: {
40+
get: (opts: { data: T }) => Promise<T>
41+
post: (opts: { data: T }) => Promise<T>
42+
}
43+
data: {
44+
value: T
45+
type: string
46+
}
47+
}
48+
49+
interface TestProps<T> extends PrimitiveComponentProps<T> {
50+
method: 'get' | 'post'
51+
}
52+
function Test<T>(props: TestProps<T>) {
53+
const queryFn = useCallback(async () => {
54+
const result = await props.serverFn[props.method]({
55+
data: props.data.value,
56+
})
57+
if (result === undefined) {
58+
return '$undefined'
59+
}
60+
return result
61+
}, [props])
62+
const query = useQuery({ queryKey: [props.data.type, props.method], queryFn })
63+
const testId = `${props.method}-${props.data.type}`
64+
return (
65+
<div>
66+
<h3>serverFn method={props.method}</h3>
67+
<h4> expected </h4>
68+
<div data-testid={`expected-${testId}`}>
69+
{stringify(props.data.value)}
70+
</div>
71+
<h4> result</h4>
72+
{query.isSuccess ? (
73+
<div data-testid={`result-${testId}`}>{stringify(query.data)}</div>
74+
) : null}
75+
</div>
76+
)
77+
}
78+
function PrimitiveComponent<T>(props: PrimitiveComponentProps<T>) {
79+
return (
80+
<div>
81+
<h2>data type: {props.data.type}</h2>
82+
<Test {...props} method="post" />
83+
<br />
84+
<Test {...props} method="get" />
85+
<br />
86+
<br />
87+
</div>
88+
)
89+
}
90+
91+
function makeTestCase<T>(props: PrimitiveComponentProps<T>) {
92+
return props
93+
}
94+
const testCases = [
95+
makeTestCase({
96+
data: {
97+
value: null,
98+
type: 'null',
99+
},
100+
serverFn: {
101+
get: $nullGet,
102+
post: $nullPost,
103+
},
104+
}),
105+
makeTestCase({
106+
data: {
107+
value: undefined,
108+
type: 'undefined',
109+
},
110+
serverFn: {
111+
get: $undefinedGet,
112+
post: $undefinedPost,
113+
},
114+
}),
115+
makeTestCase({
116+
data: {
117+
value: 'foo-bar',
118+
type: 'string',
119+
},
120+
serverFn: {
121+
get: $stringGet,
122+
post: $stringPost,
123+
},
124+
}),
125+
] as Array<PrimitiveComponentProps<any>>
126+
127+
function RouteComponent() {
128+
return testCases.map((t) => <PrimitiveComponent {...t} key={t.data.type} />)
129+
}

e2e/react-start/server-functions/tests/server-functions.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,26 @@ test('factory', async ({ page }) => {
443443
)
444444
}
445445
})
446+
447+
test('primitives', async ({ page }) => {
448+
await page.goto('/primitives')
449+
450+
const testCases = await page
451+
.locator('[data-testid^="expected-"]')
452+
.elementHandles()
453+
for (const testCase of testCases) {
454+
const testId = await testCase.getAttribute('data-testid')
455+
456+
if (!testId) {
457+
throw new Error('testcase is missing data-testid')
458+
}
459+
460+
const suffix = testId.replace('expected-', '')
461+
462+
const expected =
463+
(await page.getByTestId(`expected-${suffix}`).textContent()) || ''
464+
expect(expected).not.toBe('')
465+
466+
await expect(page.getByTestId(`result-${suffix}`)).toContainText(expected)
467+
}
468+
})

0 commit comments

Comments
 (0)