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..10c307b22f2 --- /dev/null +++ b/packages/react-start/src/tests/useServerFnMutation.test-d.tsx @@ -0,0 +1,36 @@ +import { useMutation } from '@tanstack/react-query' +import { createServerFn, useServerFn } from '../index' + +const serverFn = createServerFn({ method: 'POST' }) + .inputValidator((input: { name: string }) => input) + .handler(async ({ data }) => ({ + message: `Hello ${data.name}!`, + })) + +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 + }, + }) + + void mutation + return null +} + +export function useOptionalServerFnRegressionHook() { + const optionalHandler = useServerFn(optionalServerFn) + + optionalHandler().then((result) => { + result.ok + }) + + void optionalHandler() + void optionalHandler(undefined) +} diff --git a/packages/react-start/src/useServerFn.ts b/packages/react-start/src/useServerFn.ts index 67a09a4358c..57e4a1a4278 100644 --- a/packages/react-start/src/useServerFn.ts +++ b/packages/react-start/src/useServerFn.ts @@ -1,13 +1,30 @@ import * as React from 'react' import { isRedirect, useRouter } from '@tanstack/react-router' +type AwaitedReturn) => Promise> = Awaited< + ReturnType +> + +type NonUndefined = Exclude + +type UseServerFnReturn) => Promise> = + Parameters extends [] + ? () => Promise> + : Parameters extends [infer TVariables] + ? undefined extends TVariables + ? [NonUndefined] extends [never] + ? () => Promise> + : (variables?: NonUndefined) => Promise> + : (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 +32,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/tests/useServerFnMutation.test-d.tsx b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx new file mode 100644 index 00000000000..e0558eae176 --- /dev/null +++ b/packages/solid-start/src/tests/useServerFnMutation.test-d.tsx @@ -0,0 +1,33 @@ +import { createServerFn, useServerFn } from '../index' + +const serverFn = createServerFn({ method: 'POST' }) + .inputValidator((input: { name: string }) => input) + .handler(async ({ data }) => ({ + message: `Hello ${data.name}!`, + })) + +const optionalServerFn = createServerFn().handler(async () => ({ + 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) +} diff --git a/packages/solid-start/src/useServerFn.ts b/packages/solid-start/src/useServerFn.ts index b0949121b40..5a51da571ef 100644 --- a/packages/solid-start/src/useServerFn.ts +++ b/packages/solid-start/src/useServerFn.ts @@ -1,11 +1,28 @@ import { isRedirect, useRouter } from '@tanstack/solid-router' +type AwaitedReturn) => Promise> = Awaited< + ReturnType +> + +type NonUndefined = Exclude + +type UseServerFnReturn) => Promise> = + Parameters extends [] + ? () => Promise> + : Parameters extends [infer TVariables] + ? undefined extends TVariables + ? [NonUndefined] extends [never] + ? () => Promise> + : (variables?: NonUndefined) => Promise> + : (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 +30,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 }