Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions e2e/start/basic/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { Route as rootRoute } from './routes/__root'
import { Route as UsersImport } from './routes/users'
import { Route as StatusImport } from './routes/status'
import { Route as ServerFnsImport } from './routes/server-fns'
import { Route as SearchParamsImport } from './routes/search-params'
import { Route as RedirectImport } from './routes/redirect'
import { Route as PostsImport } from './routes/posts'
Expand Down Expand Up @@ -42,6 +43,12 @@ const StatusRoute = StatusImport.update({
getParentRoute: () => rootRoute,
} as any)

const ServerFnsRoute = ServerFnsImport.update({
id: '/server-fns',
path: '/server-fns',
getParentRoute: () => rootRoute,
} as any)

const SearchParamsRoute = SearchParamsImport.update({
id: '/search-params',
path: '/search-params',
Expand Down Expand Up @@ -170,6 +177,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SearchParamsImport
parentRoute: typeof rootRoute
}
'/server-fns': {
id: '/server-fns'
path: '/server-fns'
fullPath: '/server-fns'
preLoaderRoute: typeof ServerFnsImport
parentRoute: typeof rootRoute
}
'/status': {
id: '/status'
path: '/status'
Expand Down Expand Up @@ -301,6 +315,7 @@ export interface FileRoutesByFullPath {
'/posts': typeof PostsRouteWithChildren
'/redirect': typeof RedirectRoute
'/search-params': typeof SearchParamsRoute
'/server-fns': typeof ServerFnsRoute
'/status': typeof StatusRoute
'/users': typeof UsersRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
Expand All @@ -318,6 +333,7 @@ export interface FileRoutesByTo {
'/deferred': typeof DeferredRoute
'/redirect': typeof RedirectRoute
'/search-params': typeof SearchParamsRoute
'/server-fns': typeof ServerFnsRoute
'/status': typeof StatusRoute
'/posts/$postId': typeof PostsPostIdRoute
'/users/$userId': typeof UsersUserIdRoute
Expand All @@ -336,6 +352,7 @@ export interface FileRoutesById {
'/posts': typeof PostsRouteWithChildren
'/redirect': typeof RedirectRoute
'/search-params': typeof SearchParamsRoute
'/server-fns': typeof ServerFnsRoute
'/status': typeof StatusRoute
'/users': typeof UsersRouteWithChildren
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
Expand All @@ -357,6 +374,7 @@ export interface FileRouteTypes {
| '/posts'
| '/redirect'
| '/search-params'
| '/server-fns'
| '/status'
| '/users'
| '/posts/$postId'
Expand All @@ -373,6 +391,7 @@ export interface FileRouteTypes {
| '/deferred'
| '/redirect'
| '/search-params'
| '/server-fns'
| '/status'
| '/posts/$postId'
| '/users/$userId'
Expand All @@ -389,6 +408,7 @@ export interface FileRouteTypes {
| '/posts'
| '/redirect'
| '/search-params'
| '/server-fns'
| '/status'
| '/users'
| '/_layout/_layout-2'
Expand All @@ -409,6 +429,7 @@ export interface RootRouteChildren {
PostsRoute: typeof PostsRouteWithChildren
RedirectRoute: typeof RedirectRoute
SearchParamsRoute: typeof SearchParamsRoute
ServerFnsRoute: typeof ServerFnsRoute
StatusRoute: typeof StatusRoute
UsersRoute: typeof UsersRouteWithChildren
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
Expand All @@ -421,6 +442,7 @@ const rootRouteChildren: RootRouteChildren = {
PostsRoute: PostsRouteWithChildren,
RedirectRoute: RedirectRoute,
SearchParamsRoute: SearchParamsRoute,
ServerFnsRoute: ServerFnsRoute,
StatusRoute: StatusRoute,
UsersRoute: UsersRouteWithChildren,
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
Expand All @@ -442,6 +464,7 @@ export const routeTree = rootRoute
"/posts",
"/redirect",
"/search-params",
"/server-fns",
"/status",
"/users",
"/posts_/$postId/deep"
Expand Down Expand Up @@ -472,6 +495,9 @@ export const routeTree = rootRoute
"/search-params": {
"filePath": "search-params.tsx"
},
"/server-fns": {
"filePath": "server-fns.tsx"
},
"/status": {
"filePath": "status.tsx"
},
Expand Down
124 changes: 124 additions & 0 deletions e2e/start/basic/app/routes/server-fns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'

export const Route = createFileRoute('/server-fns')({
component: RouteComponent,
})

function RouteComponent() {
return (
<>
<ConsistentServerFnCalls />
</>
)
}

// START CONSISTENT_SERVER_FN_CALLS
const cons_getFn1 = createServerFn()
.validator((d: { username: string }) => d)
.handler(async ({ data }) => {
return { payload: data }
})

const cons_serverGetFn1 = createServerFn()
.validator((d: { username: string }) => d)
.handler(({ data }) => {
return cons_getFn1({ data })
})

const cons_postFn1 = createServerFn({ method: 'POST' })
.validator((d: { username: string }) => d)
.handler(async ({ data }) => {
return { payload: data }
})

const cons_serverPostFn1 = createServerFn({ method: 'POST' })
.validator((d: { username: string }) => d)
.handler(({ data }) => {
return cons_postFn1({ data })
})

/**
* This component checks whether the returned payloads from server function
* are the same, regardless of whether the server function is called directly
* from the client or from within the server function.
* @link https://github.com/TanStack/router/issues/1866
* @link https://github.com/TanStack/router/issues/2481
*/
function ConsistentServerFnCalls() {
const [getServerResult, setGetServerResult] = React.useState({})
const [getDirectResult, setGetDirectResult] = React.useState({})

const [postServerResult, setPostServerResult] = React.useState({})
const [postDirectResult, setPostDirectResult] = React.useState({})

return (
<div className="p-2 border m-2 grid gap-2">
<h3>Consistent Server Fn GET Calls</h3>
<p>
This component checks whether the returned payloads from server function
are the same, regardless of whether the server function is called
directly from the client or from within the server function.
</p>
<div>
It should return{' '}
<code>
<pre data-testid="expected-consistent-server-fns-result">
{JSON.stringify({ payload: { username: 'TEST' } })}
</pre>
</code>
</div>
<p>
{`GET: cons_getFn1 called from server cons_serverGetFn1 returns`}
<br />
<span data-testid="cons_serverGetFn1-response">
{JSON.stringify(getServerResult)}
</span>
</p>
<p>
{`GET: cons_getFn1 called directly returns`}
<br />
<span data-testid="cons_getFn1-response">
{JSON.stringify(getDirectResult)}
</span>
</p>
<p>
{`POST: cons_postFn1 called from cons_serverPostFn1 returns`}
<br />
<span data-testid="cons_serverPostFn1-response">
{JSON.stringify(postServerResult)}
</span>
</p>
<p>
{`POST: cons_postFn1 called directly returns`}
<br />
<span data-testid="cons_postFn1-response">
{JSON.stringify(postDirectResult)}
</span>
</p>
<button
data-testid="test-consistent-server-fn-calls-btn"
type="button"
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={() => {
// GET calls
cons_serverGetFn1({ data: { username: 'TEST' } }).then(
setGetServerResult,
)
cons_getFn1({ data: { username: 'TEST' } }).then(setGetDirectResult)

// POST calls
cons_serverPostFn1({ data: { username: 'TEST' } }).then(
setPostServerResult,
)
cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult)
}}
>
Test Consistent server function responses
</button>
</div>
)
}

// END CONSISTENT_SERVER_FN_CALLS
30 changes: 30 additions & 0 deletions e2e/start/basic/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,33 @@ test('invoking a server function with custom response status code', async ({
})
await requestPromise
})

test('Consistent server function returns both on client and server for GET and POST calls', async ({
page,
}) => {
await page.goto('/server-fns')

await page.waitForLoadState('networkidle')
const expected =
(await page
.getByTestId('expected-consistent-server-fns-result')
.textContent()) || ''
expect(expected).not.toBe('')

await page.getByTestId('test-consistent-server-fn-calls-btn').click()
await page.waitForLoadState('networkidle')

// GET calls
await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText(
expected,
)
await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected)

// POST calls
await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText(
expected,
)
await expect(page.getByTestId('cons_postFn1-response')).toContainText(
expected,
)
})
5 changes: 4 additions & 1 deletion packages/start/src/client-runtime/fetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export async function fetcher(
body:
type === 'formData'
? first.data
: (defaultTransformer.stringify(first.data ?? null) as any),
: (defaultTransformer.stringify({
data: first.data ?? null,
context: first.context,
}) as any),
}
: {}),
})
Expand Down
4 changes: 4 additions & 0 deletions packages/start/src/client/createServerFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export function createServerFn<
TValidator
>

if (typeof resolvedOptions.method === 'undefined') {
resolvedOptions.method = 'GET' as TMethod
}

return {
options: resolvedOptions as any,
middleware: (middleware) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/start/src/server-handler/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export async function handleServerRequest(request: Request, _event?: H3Event) {
}

// For non-form, non-get
return await request.json()
const jsonPayloadAsString = await request.text()
return defaultTransformer.parse(jsonPayloadAsString)
})()

const result = await action(arg)
Expand Down