-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fixed infered type from useServerFn + added type test #5367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
008ee96
0cf8f24
ac1e342
7104281
1a7438d
745afd3
aea930f
37e55f3
2e28196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
) | ||
} |
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) | ||
} |
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> | ||
} |
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) => { | ||||||
result.message | ||||||
}) | ||||||
|
||||||
void handler | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete void context test. Line 20 uses Apply this diff to call the handler in a void context: - void handler
+ void handler({ name: 'TanStack' }) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||
return null | ||||||
} | ||||||
|
||||||
export function useOptionalServerFnRegressionHook() { | ||||||
const handler = useServerFn(optionalServerFn) | ||||||
|
||||||
handler().then((result) => { | ||||||
result.ok | ||||||
}) | ||||||
|
||||||
void handler() | ||||||
void handler(undefined) | ||||||
} |
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> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the input type mismatch.
The handler is being called with
{ data: { name: 'TanStack' } }
, but the input validator (line 4) expects{ name: string }
. Thedata
property wrapper is used server-side within the handler, not in the client-side call signature.Apply this diff to fix the call:
📝 Committable suggestion
🤖 Prompt for AI Agents