From 008ee96900e8854d44f7957665dc02b6b250e4e1 Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 14:22:44 +1100 Subject: [PATCH 1/9] fixed infered type from useServerFn + added type test --- .../src/routes/index.tsx | 28 +++++++++++++++++-- .../src/tests/useServerFnMutation.test-d.tsx | 15 ++++++++++ packages/react-start/src/useServerFn.ts | 27 ++++++++++++++---- packages/solid-start/src/useServerFn.ts | 25 +++++++++++++---- 4 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 packages/react-start/src/tests/useServerFnMutation.test-d.tsx diff --git a/examples/react/start-basic-react-query/src/routes/index.tsx b/examples/react/start-basic-react-query/src/routes/index.tsx index 37c8d237bd4..1d6eba35fae 100644 --- a/examples/react/start-basic-react-query/src/routes/index.tsx +++ b/examples/react/start-basic-react-query/src/routes/index.tsx @@ -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 ( -
-

Welcome Home!!!

+
+

useServerFn + useMutation demo

+ + {greetingMutation.data ? ( +

{greetingMutation.data.message}

+ ) : null}
) } diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx new file mode 100644 index 00000000000..cca1bef2c8f --- /dev/null +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query' +import { createServerFn, useServerFn } from '@tanstack/react-start' + +const serverFn = createServerFn({ method: 'POST' }) + .inputValidator((input: { name: string }) => input) + .handler(async ({ data }) => ({ + message: `Hello ${data.name}!`, + })) + +useMutation({ + mutationFn: useServerFn(serverFn), + onSuccess: (data) => { + data.message + }, +}) diff --git a/packages/react-start/src/useServerFn.ts b/packages/react-start/src/useServerFn.ts index 67a09a4358c..f62900afabb 100644 --- a/packages/react-start/src/useServerFn.ts +++ b/packages/react-start/src/useServerFn.ts @@ -1,13 +1,24 @@ import * as React from 'react' import { isRedirect, useRouter } from '@tanstack/react-router' +type AwaitedReturn) => Promise> = Awaited< + ReturnType +> + +type UseServerFnReturn) => Promise> = + Parameters extends [] + ? () => Promise> + : Parameters extends [infer TVariables] + ? (variables: TVariables) => Promise> + : (...args: Parameters) => Promise> + export function useServerFn) => Promise>( serverFn: T, -): (...args: Parameters) => ReturnType { +): UseServerFnReturn { const router = useRouter() - return React.useCallback( - async (...args: Array) => { + const handler = React.useCallback( + async (...args: Parameters) => { try { const res = await serverFn(...args) @@ -15,16 +26,20 @@ export function useServerFn) => Promise>( throw res } - return res + return res as AwaitedReturn } 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 } throw err } }, [router, serverFn], - ) as any + ) + + return handler as UseServerFnReturn } diff --git a/packages/solid-start/src/useServerFn.ts b/packages/solid-start/src/useServerFn.ts index b0949121b40..d7db552d153 100644 --- a/packages/solid-start/src/useServerFn.ts +++ b/packages/solid-start/src/useServerFn.ts @@ -1,11 +1,22 @@ import { isRedirect, useRouter } from '@tanstack/solid-router' +type AwaitedReturn) => Promise> = Awaited< + ReturnType +> + +type UseServerFnReturn) => Promise> = + Parameters extends [] + ? () => Promise> + : Parameters extends [infer TVariables] + ? (variables: TVariables) => Promise> + : (...args: Parameters) => Promise> + export function useServerFn) => Promise>( serverFn: T, -): (...args: Parameters) => ReturnType { +): UseServerFnReturn { const router = useRouter() - return (async (...args: Array) => { + const handler = async (...args: Parameters) => { try { const res = await serverFn(...args) @@ -13,14 +24,18 @@ export function useServerFn) => Promise>( throw res } - return res + return res as AwaitedReturn } 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 } throw err } - }) as any + } + + return handler as UseServerFnReturn } From 0cf8f24f1b8b2dee399349ac2cc22829535e7c15 Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 14:40:37 +1100 Subject: [PATCH 2/9] fixed wrong import --- packages/react-start/src/tests/useServerFnMutation.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index cca1bef2c8f..1d944d32058 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query' -import { createServerFn, useServerFn } from '@tanstack/react-start' +import { createServerFn, useServerFn } from '../index' const serverFn = createServerFn({ method: 'POST' }) .inputValidator((input: { name: string }) => input) From ac1e3422d2cfca32e2897944de9105b45be49215 Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 20:55:06 +1100 Subject: [PATCH 3/9] fixed types to keep single-argument optionals optional --- .../src/tests/useServerFnMutation.test-d.tsx | 11 +++++++++++ packages/react-start/src/useServerFn.ts | 10 +++++++++- packages/solid-start/src/useServerFn.ts | 10 +++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index 1d944d32058..65f04b4ab9b 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -13,3 +13,14 @@ useMutation({ data.message }, }) + +const optionalServerFn = createServerFn() + .handler(async () => ({ + ok: true as const, + })) + +useServerFn(optionalServerFn)().then((result) => { + result.ok +}) + +useServerFn(optionalServerFn)({ headers: { 'x-test': '1' } }) diff --git a/packages/react-start/src/useServerFn.ts b/packages/react-start/src/useServerFn.ts index f62900afabb..8a420f02823 100644 --- a/packages/react-start/src/useServerFn.ts +++ b/packages/react-start/src/useServerFn.ts @@ -5,11 +5,19 @@ type AwaitedReturn) => Promise> = Awaited< ReturnType > +type NonUndefined = Exclude + type UseServerFnReturn) => Promise> = Parameters extends [] ? () => Promise> : Parameters extends [infer TVariables] - ? (variables: TVariables) => Promise> + ? undefined extends TVariables + ? [NonUndefined] extends [never] + ? () => Promise> + : ( + variables?: NonUndefined, + ) => Promise> + : (variables: TVariables) => Promise> : (...args: Parameters) => Promise> export function useServerFn) => Promise>( diff --git a/packages/solid-start/src/useServerFn.ts b/packages/solid-start/src/useServerFn.ts index d7db552d153..0e75d73f0e6 100644 --- a/packages/solid-start/src/useServerFn.ts +++ b/packages/solid-start/src/useServerFn.ts @@ -4,11 +4,19 @@ type AwaitedReturn) => Promise> = Awaited< ReturnType > +type NonUndefined = Exclude + type UseServerFnReturn) => Promise> = Parameters extends [] ? () => Promise> : Parameters extends [infer TVariables] - ? (variables: TVariables) => Promise> + ? undefined extends TVariables + ? [NonUndefined] extends [never] + ? () => Promise> + : ( + variables?: NonUndefined, + ) => Promise> + : (variables: TVariables) => Promise> : (...args: Parameters) => Promise> export function useServerFn) => Promise>( From 71042813aa07d96ec892cf707199f6b01ff5e2c8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 09:56:03 +0000 Subject: [PATCH 4/9] ci: apply automated fixes --- .../react-start/src/tests/useServerFnMutation.test-d.tsx | 7 +++---- packages/react-start/src/useServerFn.ts | 4 +--- packages/solid-start/src/useServerFn.ts | 4 +--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index 65f04b4ab9b..672b5dd2670 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -14,10 +14,9 @@ useMutation({ }, }) -const optionalServerFn = createServerFn() - .handler(async () => ({ - ok: true as const, - })) +const optionalServerFn = createServerFn().handler(async () => ({ + ok: true as const, +})) useServerFn(optionalServerFn)().then((result) => { result.ok diff --git a/packages/react-start/src/useServerFn.ts b/packages/react-start/src/useServerFn.ts index 8a420f02823..57e4a1a4278 100644 --- a/packages/react-start/src/useServerFn.ts +++ b/packages/react-start/src/useServerFn.ts @@ -14,9 +14,7 @@ type UseServerFnReturn) => Promise> = ? undefined extends TVariables ? [NonUndefined] extends [never] ? () => Promise> - : ( - variables?: NonUndefined, - ) => Promise> + : (variables?: NonUndefined) => Promise> : (variables: TVariables) => Promise> : (...args: Parameters) => Promise> diff --git a/packages/solid-start/src/useServerFn.ts b/packages/solid-start/src/useServerFn.ts index 0e75d73f0e6..5a51da571ef 100644 --- a/packages/solid-start/src/useServerFn.ts +++ b/packages/solid-start/src/useServerFn.ts @@ -13,9 +13,7 @@ type UseServerFnReturn) => Promise> = ? undefined extends TVariables ? [NonUndefined] extends [never] ? () => Promise> - : ( - variables?: NonUndefined, - ) => Promise> + : (variables?: NonUndefined) => Promise> : (variables: TVariables) => Promise> : (...args: Parameters) => Promise> From 1a7438d890d7675fcdefc3177fa7859c1ccf14e4 Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 21:22:28 +1100 Subject: [PATCH 5/9] clean up --- .../src/tests/useServerFnMutation.test-d.tsx | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index 672b5dd2670..5aaba41f28a 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -3,23 +3,39 @@ import { createServerFn, useServerFn } from '../index' const serverFn = createServerFn({ method: 'POST' }) .inputValidator((input: { name: string }) => input) - .handler(async ({ data }) => ({ - message: `Hello ${data.name}!`, - })) + .handler(async ({ data }) => { + return await Promise.resolve({ + message: `Hello ${data.name}!`, + }) + }) -useMutation({ - mutationFn: useServerFn(serverFn), - onSuccess: (data) => { - data.message - }, -}) +const optionalServerFn = createServerFn() + .handler(async () => { + return await Promise.resolve({ + ok: true as const, + }) + }) -const optionalServerFn = createServerFn().handler(async () => ({ - ok: true as const, -})) +export function UseServerFnMutationRegressionComponent() { + const mutation = useMutation({ + mutationFn: useServerFn(serverFn), + onSuccess: (data, variables) => { + data.message + variables?.data?.name + }, + }) -useServerFn(optionalServerFn)().then((result) => { - result.ok -}) + void mutation + return null +} -useServerFn(optionalServerFn)({ headers: { 'x-test': '1' } }) +export function useOptionalServerFnRegressionHook() { + const optionalHandler = useServerFn(optionalServerFn) + + optionalHandler().then((result) => { + result.ok + }) + + void optionalHandler() + void optionalHandler(undefined) +} From 745afd3bd75917b5d3ded3bb55f513d6614b309f Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 21:23:10 +1100 Subject: [PATCH 6/9] added solid test --- .../src/tests/useServerFnMutation.test-d.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/solid-start/src/tests/useServerFnMutation.test-d.tsx diff --git a/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx new file mode 100644 index 00000000000..85636f869e3 --- /dev/null +++ b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx @@ -0,0 +1,38 @@ +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) => { + result.message + }) + + void handler + return null +} + +export function useOptionalServerFnRegressionHook() { + const handler = useServerFn(optionalServerFn) + + handler().then((result) => { + result.ok + }) + + void handler() + void handler(undefined) +} From aea930fb4fff8882a048fb567a0ad897d84a64d2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 10:28:12 +0000 Subject: [PATCH 7/9] ci: apply automated fixes --- .../react-start/src/tests/useServerFnMutation.test-d.tsx | 9 ++++----- .../solid-start/src/tests/useServerFnMutation.test-d.tsx | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index 5aaba41f28a..47d4d24f797 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -9,12 +9,11 @@ const serverFn = createServerFn({ method: 'POST' }) }) }) -const optionalServerFn = createServerFn() - .handler(async () => { - return await Promise.resolve({ - ok: true as const, - }) +const optionalServerFn = createServerFn().handler(async () => { + return await Promise.resolve({ + ok: true as const, }) +}) export function UseServerFnMutationRegressionComponent() { const mutation = useMutation({ diff --git a/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx index 85636f869e3..0d4a145dded 100644 --- a/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx @@ -8,12 +8,11 @@ const serverFn = createServerFn({ method: 'POST' }) }) }) -const optionalServerFn = createServerFn() - .handler(async () => { - return await Promise.resolve({ - ok: true as const, - }) +const optionalServerFn = createServerFn().handler(async () => { + return await Promise.resolve({ + ok: true as const, }) +}) export function UseServerFnRegressionComponent() { const handler = useServerFn(serverFn) From 37e55f3c6bd43982e011b201c5ba7cd676b9eccd Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 21:33:13 +1100 Subject: [PATCH 8/9] removed unnecessary chaining operator --- packages/react-start/src/tests/useServerFnMutation.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index 47d4d24f797..d8a1a29688c 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -20,7 +20,7 @@ export function UseServerFnMutationRegressionComponent() { mutationFn: useServerFn(serverFn), onSuccess: (data, variables) => { data.message - variables?.data?.name + variables.data.name }, }) From 2e28196fd48083b81d66623a1a63f4534acb5f56 Mon Sep 17 00:00:00 2001 From: Christopher Menendez Date: Sun, 5 Oct 2025 21:58:52 +1100 Subject: [PATCH 9/9] fixed nit picks --- .../src/tests/useServerFnMutation.test-d.tsx | 16 ++++++---------- .../src/tests/useServerFnMutation.test-d.tsx | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx index d8a1a29688c..10c307b22f2 100644 --- a/packages/react-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -3,17 +3,13 @@ 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}!`, - }) - }) + .handler(async ({ data }) => ({ + message: `Hello ${data.name}!`, + })) -const optionalServerFn = createServerFn().handler(async () => { - return await Promise.resolve({ - ok: true as const, - }) -}) +const optionalServerFn = createServerFn().handler(async () => ({ + ok: true as const, +})) export function UseServerFnMutationRegressionComponent() { const mutation = useMutation({ diff --git a/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx index 0d4a145dded..e0558eae176 100644 --- a/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx +++ b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx @@ -2,17 +2,13 @@ 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}!`, - }) - }) + .handler(async ({ data }) => ({ + message: `Hello ${data.name}!`, + })) -const optionalServerFn = createServerFn().handler(async () => { - return await Promise.resolve({ - ok: true as const, - }) -}) +const optionalServerFn = createServerFn().handler(async () => ({ + ok: true as const, +})) export function UseServerFnRegressionComponent() { const handler = useServerFn(serverFn)