Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
28 changes: 26 additions & 2 deletions examples/react/start-basic-react-query/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import * as React from 'react'
import { useMutation } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn, useServerFn } from '@tanstack/react-start'

const greetUser = createServerFn({ method: 'POST' })
.inputValidator((input: { name: string }) => input)
.handler(async ({ data }) => {
return Promise.resolve({ message: `Hello ${data.name}!` })
})

export const Route = createFileRoute('/')({
component: Home,
})

function Home() {
const greetingMutation = useMutation({
mutationFn: useServerFn(greetUser),
onSuccess: (data) => data,
})

return (
<div className="p-2">
<h3>Welcome Home!!!</h3>
<div className="p-2 flex flex-col gap-2">
<h3>useServerFn + useMutation demo</h3>
<button
className="rounded bg-blue-500 px-3 py-1 text-white"
onClick={() => greetingMutation.mutate({ data: { name: 'TanStack' } })}
>
Say hi
</button>
{greetingMutation.data ? (
<p data-testid="greeted">{greetingMutation.data.message}</p>
) : null}
</div>
)
}
40 changes: 40 additions & 0 deletions packages/react-start/src/tests/useServerFnMutation.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useMutation } from '@tanstack/react-query'
import { createServerFn, useServerFn } from '../index'

const serverFn = createServerFn({ method: 'POST' })
.inputValidator((input: { name: string }) => input)
.handler(async ({ data }) => {
return await Promise.resolve({
message: `Hello ${data.name}!`,
})
})

const optionalServerFn = createServerFn().handler(async () => {
return await Promise.resolve({
ok: true as const,
})
})

export function UseServerFnMutationRegressionComponent() {
const mutation = useMutation({
mutationFn: useServerFn(serverFn),
onSuccess: (data, variables) => {
data.message
variables?.data?.name
},
})

void mutation
return null
}

export function useOptionalServerFnRegressionHook() {
const optionalHandler = useServerFn(optionalServerFn)

optionalHandler().then((result) => {
result.ok
})

void optionalHandler()
void optionalHandler(undefined)
}
33 changes: 27 additions & 6 deletions packages/react-start/src/useServerFn.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
import * as React from 'react'
import { isRedirect, useRouter } from '@tanstack/react-router'

type AwaitedReturn<T extends (...args: Array<any>) => Promise<any>> = Awaited<
ReturnType<T>
>

type NonUndefined<T> = Exclude<T, undefined>

type UseServerFnReturn<T extends (...args: Array<any>) => Promise<any>> =
Parameters<T> extends []
? () => Promise<AwaitedReturn<T>>
: Parameters<T> extends [infer TVariables]
? undefined extends TVariables
? [NonUndefined<TVariables>] extends [never]
? () => Promise<AwaitedReturn<T>>
: (variables?: NonUndefined<TVariables>) => Promise<AwaitedReturn<T>>
: (variables: TVariables) => Promise<AwaitedReturn<T>>
: (...args: Parameters<T>) => Promise<AwaitedReturn<T>>

export function useServerFn<T extends (...deps: Array<any>) => Promise<any>>(
serverFn: T,
): (...args: Parameters<T>) => ReturnType<T> {
): UseServerFnReturn<T> {
const router = useRouter()

return React.useCallback(
async (...args: Array<any>) => {
const handler = React.useCallback(
async (...args: Parameters<T>) => {
try {
const res = await serverFn(...args)

if (isRedirect(res)) {
throw res
}

return res
return res as AwaitedReturn<T>
} catch (err) {
if (isRedirect(err)) {
err.options._fromLocation = router.state.location
return router.navigate(router.resolveRedirect(err).options)
return router.navigate(
router.resolveRedirect(err).options,
) as AwaitedReturn<T>
}

throw err
}
},
[router, serverFn],
) as any
)

return handler as UseServerFnReturn<T>
}
37 changes: 37 additions & 0 deletions packages/solid-start/src/tests/useServerFnMutation.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createServerFn, useServerFn } from '../index'

const serverFn = createServerFn({ method: 'POST' })
.inputValidator((input: { name: string }) => input)
.handler(async ({ data }) => {
return await Promise.resolve({
message: `Hello ${data.name}!`,
})
})

const optionalServerFn = createServerFn().handler(async () => {
return await Promise.resolve({
ok: true as const,
})
})

export function UseServerFnRegressionComponent() {
const handler = useServerFn(serverFn)

handler({ data: { name: 'TanStack' } }).then((result) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the input type mismatch.

The handler is being called with { data: { name: 'TanStack' } }, but the input validator (line 4) expects { name: string }. The data property wrapper is used server-side within the handler, not in the client-side call signature.

Apply this diff to fix the call:

-  handler({ data: { name: 'TanStack' } }).then((result) => {
+  handler({ name: 'TanStack' }).then((result) => {
     result.message
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handler({ data: { name: 'TanStack' } }).then((result) => {
handler({ name: 'TanStack' }).then((result) => {
result.message
})
🤖 Prompt for AI Agents
In packages/solid-start/src/tests/useServerFnMutation.test-d.tsx around line 20,
the test calls handler({ data: { name: 'TanStack' } }) but the input validator
expects a plain { name: string }; remove the unnecessary data wrapper and call
handler({ name: 'TanStack' }) instead so the argument matches the declared input
type used by the validator.

result.message
})

void handler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incomplete void context test.

Line 20 uses void handler which only references the handler without calling it. This doesn't effectively test the handler's behavior in a void context. Based on the React equivalent test (lines 31-32 in the relevant snippets), this should call the handler.

Apply this diff to call the handler in a void context:

-  void handler
+  void handler({ name: 'TanStack' })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void handler
void handler({ name: 'TanStack' })
🤖 Prompt for AI Agents
In packages/solid-start/src/tests/useServerFnMutation.test-d.tsx around line 20,
the test currently uses "void handler" which only references the handler instead
of invoking it; change this to call the handler in a void context (e.g., use
"void handler()" so the handler is executed) to match the React equivalent test
and properly verify void-context behavior.

return null
}

export function useOptionalServerFnRegressionHook() {
const handler = useServerFn(optionalServerFn)

handler().then((result) => {
result.ok
})

void handler()
void handler(undefined)
}
31 changes: 26 additions & 5 deletions packages/solid-start/src/useServerFn.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
import { isRedirect, useRouter } from '@tanstack/solid-router'

type AwaitedReturn<T extends (...args: Array<any>) => Promise<any>> = Awaited<
ReturnType<T>
>

type NonUndefined<T> = Exclude<T, undefined>

type UseServerFnReturn<T extends (...args: Array<any>) => Promise<any>> =
Parameters<T> extends []
? () => Promise<AwaitedReturn<T>>
: Parameters<T> extends [infer TVariables]
? undefined extends TVariables
? [NonUndefined<TVariables>] extends [never]
? () => Promise<AwaitedReturn<T>>
: (variables?: NonUndefined<TVariables>) => Promise<AwaitedReturn<T>>
: (variables: TVariables) => Promise<AwaitedReturn<T>>
: (...args: Parameters<T>) => Promise<AwaitedReturn<T>>

export function useServerFn<T extends (...deps: Array<any>) => Promise<any>>(
serverFn: T,
): (...args: Parameters<T>) => ReturnType<T> {
): UseServerFnReturn<T> {
const router = useRouter()

return (async (...args: Array<any>) => {
const handler = async (...args: Parameters<T>) => {
try {
const res = await serverFn(...args)

if (isRedirect(res)) {
throw res
}

return res
return res as AwaitedReturn<T>
} catch (err) {
if (isRedirect(err)) {
err.options._fromLocation = router.state.location
return router.navigate(router.resolveRedirect(err).options)
return router.navigate(
router.resolveRedirect(err).options,
) as AwaitedReturn<T>
}

throw err
}
}) as any
}

return handler as UseServerFnReturn<T>
}
Loading