Repository: TanStack/router
Pull request #1747 (2024-06-13 18:55:33)

fix(react-router): match route location when pending has not been set
When pending has not been set for matchRoute we should determine the base location based on if the router is loading or not.

- If the router is loading then use the previous location like `Link`
- If the router is not loading then we can safely use the latest location.

This allows the user to conditionally render a `Link` with `from` without any errors

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 49fcd4772f09baaec9ee3b1852d754dd893322cc.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/666b40c09ec39c6cfaf88a22?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)

Patch:
diff --git a/README.md b/README.md
index b19e55e00..dead1f348 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png)
 
-🤖 Type-safe router w/ built-in caching & URL state management for React, Preact, and friends
+🤖 Type-safe router w/ built-in caching & URL state management for React!
 
 <a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
   <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
diff --git a/docs/framework/react/guide/navigation-blocking.md b/docs/framework/react/guide/navigation-blocking.md
index 3e4dc0053..e6b327e28 100644
--- a/docs/framework/react/guide/navigation-blocking.md
+++ b/docs/framework/react/guide/navigation-blocking.md
@@ -44,7 +44,7 @@ function MyComponent() {
   const [formIsDirty, setFormIsDirty] = useState(false)
 
   useBlocker({
-    blockerfn: () => window.confirm('Are you sure you want to leave?'),
+    blockerFn: () => window.confirm('Are you sure you want to leave?'),
     condition: formIsDirty,
   })
 
diff --git a/docs/framework/react/guide/ssr.md b/docs/framework/react/guide/ssr.md
index d7bd35fc1..0bfa9d780 100644
--- a/docs/framework/react/guide/ssr.md
+++ b/docs/framework/react/guide/ssr.md
@@ -39,11 +39,11 @@ Since your router will exist both on the server and the client, it's important t
 
 ```tsx
 import * as React from 'react'
-import { createRouter } from '@tanstack/react-router'
-import { routeTree } from 'routeTree.gen'
+import { createRouter as createTanstackRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
 
 export function createRouter() {
-  return createRouter({ routeTree })
+  return createTanstackRouter({ routeTree })
 }
 
 declare module '@tanstack/react-router' {
diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json
index eaeb12df5..2ccec6b01 100644
--- a/examples/react/authenticated-routes/package.json
+++ b/examples/react/authenticated-routes/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json
index 6c35f4b77..03b095765 100644
--- a/examples/react/basic-file-based-codesplitting/package.json
+++ b/examples/react/basic-file-based-codesplitting/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json
index 10f20059e..6d43dd47e 100644
--- a/examples/react/basic-file-based/package.json
+++ b/examples/react/basic-file-based/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json
index 7748d412f..1beb787aa 100644
--- a/examples/react/basic-react-query-file-based/package.json
+++ b/examples/react/basic-react-query-file-based/package.json
@@ -13,7 +13,7 @@
     "@tanstack/react-query-devtools": "^5.17.8",
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json
index e8d83289a..b453f7837 100644
--- a/examples/react/basic-ssr-file-based/package.json
+++ b/examples/react/basic-ssr-file-based/package.json
@@ -13,9 +13,9 @@
   },
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/start": "^1.38.0",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "get-port": "^7.0.0",
     "react": "^18.2.0",
diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json
index 5f6bfcaa1..70e4e35fb 100644
--- a/examples/react/basic-ssr-streaming-file-based/package.json
+++ b/examples/react/basic-ssr-streaming-file-based/package.json
@@ -13,9 +13,9 @@
   },
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/start": "^1.38.0",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "get-port": "^7.0.0",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json
index 5ec111028..cd3a17525 100644
--- a/examples/react/kitchen-sink-file-based/package.json
+++ b/examples/react/kitchen-sink-file-based/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json
index 232a3cb1d..da65463d7 100644
--- a/examples/react/kitchen-sink-react-query-file-based/package.json
+++ b/examples/react/kitchen-sink-react-query-file-based/package.json
@@ -13,7 +13,7 @@
     "@tanstack/react-query-devtools": "^5.17.8",
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json
index 86ec0453c..5c5556d3a 100644
--- a/examples/react/large-file-based/package.json
+++ b/examples/react/large-file-based/package.json
@@ -14,7 +14,7 @@
     "@tanstack/react-query": "^5.17.8",
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json
index f58289eef..8d6078240 100644
--- a/examples/react/quickstart-file-based/package.json
+++ b/examples/react/quickstart-file-based/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json
index ce1b58c7a..73f8f0c57 100644
--- a/examples/react/start-basic-rsc/package.json
+++ b/examples/react/start-basic-rsc/package.json
@@ -14,8 +14,8 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
+    "@tanstack/start": "^1.38.0",
     "redaxios": "^0.5.0",
     "isbot": "^3.7.0",
     "react": "^18.3.1",
diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json
index 015cb07bf..1cb091e85 100644
--- a/examples/react/start-basic/package.json
+++ b/examples/react/start-basic/package.json
@@ -16,8 +16,8 @@
   "dependencies": {
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
+    "@tanstack/start": "^1.38.0",
     "@typescript-eslint/parser": "^7.2.0",
     "@vitejs/plugin-react": "^4",
     "dotenv": "^16.4.5",
diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json
index 5639a44c6..78ad87dac 100644
--- a/examples/react/start-trellaux/package.json
+++ b/examples/react/start-trellaux/package.json
@@ -16,8 +16,8 @@
     "@tanstack/react-query-devtools": "^5.40.0",
     "@tanstack/react-router": "^1.36.3",
     "@tanstack/router-devtools": "^1.36.3",
-    "@tanstack/router-vite-plugin": "^1.37.0",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/router-vite-plugin": "^1.38.0",
+    "@tanstack/start": "^1.38.0",
     "@typescript-eslint/parser": "^7.2.0",
     "@vitejs/plugin-react": "^4",
     "isbot": "^3.7.0",
diff --git a/examples/react/wip-with-bling/package.json b/examples/react/wip-with-bling/package.json
index aa8eb9de4..7e41e4ea5 100644
--- a/examples/react/wip-with-bling/package.json
+++ b/examples/react/wip-with-bling/package.json
@@ -14,7 +14,7 @@
   "dependencies": {
     "@tanstack/bling": "^0.5.0",
     "@tanstack/react-router": "^1.36.3",
-    "@tanstack/start": "^1.37.0",
+    "@tanstack/start": "^1.38.0",
     "@tanstack/react-store": "^0.2.1",
     "@tanstack/router-devtools": "^1.36.3",
     "redaxios": "^0.5.0",
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index c0e581736..746001583 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -2250,7 +2250,10 @@ export class Router<
       return false
     }
 
-    const baseLocation = opts?.pending
+    const pending =
+      opts?.pending === undefined ? !this.state.isLoading : opts.pending
+
+    const baseLocation = pending
       ? this.latestLocation
       : this.state.resolvedLocation
 
diff --git a/packages/router-vite-plugin/package.json b/packages/router-vite-plugin/package.json
index 4b57050af..700bdf382 100644
--- a/packages/router-vite-plugin/package.json
+++ b/packages/router-vite-plugin/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-vite-plugin",
-  "version": "1.37.0",
+  "version": "1.38.0",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/router-vite-plugin/src/compilers.ts b/packages/router-vite-plugin/src/compilers.ts
index be130f1cb..c1ee75694 100644
--- a/packages/router-vite-plugin/src/compilers.ts
+++ b/packages/router-vite-plugin/src/compilers.ts
@@ -118,6 +118,68 @@ export async function compileFile(opts: {
                                   )
                                 }
                               }
+                            } else if (
+                              t.isIdentifier(fn) ||
+                              t.isCallExpression(fn)
+                            ) {
+                              // A function was passed to createServerFn in the form of an
+                              // identifier or a call expression that returns a function.
+
+                              // We wrap the identifier/call expression in a function
+                              // expression that accepts the same arguments as the original
+                              // function with the "use server" directive at the top of the
+                              // function scope.
+
+                              const args = t.restElement(t.identifier('args'))
+
+                              // Annotate args with the type:
+                              //  Parameters<Parameters<typeof createServerFn>[1]>
+
+                              args.typeAnnotation = t.tsTypeAnnotation(
+                                t.tsTypeReference(
+                                  t.identifier('Parameters'),
+                                  t.tsTypeParameterInstantiation([
+                                    t.tsIndexedAccessType(
+                                      t.tsTypeReference(
+                                        t.identifier('Parameters'),
+                                        t.tsTypeParameterInstantiation([
+                                          t.tsTypeQuery(
+                                            t.identifier('createServerFn'),
+                                          ),
+                                        ]),
+                                      ),
+                                      t.tsLiteralType(t.numericLiteral(1)),
+                                    ),
+                                  ]),
+                                ),
+                              )
+
+                              const wrappedFn = t.arrowFunctionExpression(
+                                [args],
+                                t.blockStatement(
+                                  [
+                                    t.returnStatement(
+                                      t.callExpression(
+                                        t.memberExpression(
+                                          fn,
+                                          t.identifier('apply'),
+                                        ),
+                                        [
+                                          t.identifier('this'),
+                                          t.identifier('args'),
+                                        ],
+                                      ),
+                                    ),
+                                  ],
+                                  [
+                                    t.directive(
+                                      t.directiveLiteral('use server'),
+                                    ),
+                                  ],
+                                ),
+                              )
+
+                              path.node.arguments[1] = wrappedFn
                             }
                           } else if (
                             path.node.callee.name === 'createRoute' ||
@@ -354,6 +416,68 @@ export async function splitFile(opts: {
                                   )
                                 }
                               }
+                            } else if (
+                              t.isIdentifier(fn) ||
+                              t.isCallExpression(fn)
+                            ) {
+                              // A function was passed to createServerFn in the form of an
+                              // identifier or a call expression that returns a function.
+
+                              // We wrap the identifier/call expression in a function
+                              // expression that accepts the same arguments as the original
+                              // function with the "use server" directive at the top of the
+                              // function scope.
+
+                              const args = t.restElement(t.identifier('args'))
+
+                              // Annotate args with the type:
+                              //  Parameters<Parameters<typeof createServerFn>[1]>
+
+                              args.typeAnnotation = t.tsTypeAnnotation(
+                                t.tsTypeReference(
+                                  t.identifier('Parameters'),
+                                  t.tsTypeParameterInstantiation([
+                                    t.tsIndexedAccessType(
+                                      t.tsTypeReference(
+                                        t.identifier('Parameters'),
+                                        t.tsTypeParameterInstantiation([
+                                          t.tsTypeQuery(
+                                            t.identifier('createServerFn'),
+                                          ),
+                                        ]),
+                                      ),
+                                      t.tsLiteralType(t.numericLiteral(1)),
+                                    ),
+                                  ]),
+                                ),
+                              )
+
+                              const wrappedFn = t.arrowFunctionExpression(
+                                [args],
+                                t.blockStatement(
+                                  [
+                                    t.returnStatement(
+                                      t.callExpression(
+                                        t.memberExpression(
+                                          fn,
+                                          t.identifier('apply'),
+                                        ),
+                                        [
+                                          t.identifier('this'),
+                                          t.identifier('args'),
+                                        ],
+                                      ),
+                                    ),
+                                  ],
+                                  [
+                                    t.directive(
+                                      t.directiveLiteral('use server'),
+                                    ),
+                                  ],
+                                ),
+                              )
+
+                              path.node.arguments[1] = wrappedFn
                             }
                           } else if (
                             path.node.callee.name === 'createRoute' ||
diff --git a/packages/start/package.json b/packages/start/package.json
index 8600fac9a..3a09d2e57 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start",
-  "version": "1.37.0",
+  "version": "1.38.0",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index 26836329f..60061fdc2 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -25,6 +25,7 @@ import {
   useParams,
   useSearch,
   useRouteContext,
+  useMatchRoute,
 } from '../src'
 
 afterEach(() => {
@@ -2724,6 +2725,113 @@ describe('Link', () => {
 
     expect(ErrorComponent).not.toHaveBeenCalled()
   })
+
+  test('when navigating from /posts to /invoices with conditionally rendering Link on the root', async () => {
+    const ErrorComponent = vi.fn(() => <div>Something went wrong!</div>)
+    const RootComponent = () => {
+      const matchRoute = useMatchRoute()
+      const matchPosts = Boolean(matchRoute({ to: '/posts' }))
+      const matchInvoices = Boolean(matchRoute({ to: '/invoices' }))
+
+      return (
+        <React.Fragment>
+          {matchPosts && (
+            <Link from="/posts" to="/posts">
+              From posts
+            </Link>
+          )}
+          {matchInvoices && (
+            <Link from="/invoices" to="/invoices">
+              From invoices
+            </Link>
+          )}
+          <Outlet />
+        </React.Fragment>
+      )
+    }
+
+    const rootRoute = createRootRoute({
+      component: RootComponent,
+      errorComponent: ErrorComponent,
+    })
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => (
+        <React.Fragment>
+          <h1>Index Route</h1>
+          <Link to="/posts">Go to posts</Link>
+        </React.Fragment>
+      ),
+    })
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: 'posts',
+      component: () => (
+        <React.Fragment>
+          <h1>On Posts</h1>
+          <Link to="/invoices">To invoices</Link>
+        </React.Fragment>
+      ),
+    })
+
+    const invoicesRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: 'invoices',
+      component: () => (
+        <React.Fragment>
+          <h1>On Invoices</h1>
+          <Link to="/posts">To posts</Link>
+        </React.Fragment>
+      ),
+    })
+
+    const routeTree = rootRoute.addChildren([
+      indexRoute,
+      postsRoute,
+      invoicesRoute,
+    ])
+
+    const router = createRouter({
+      routeTree,
+    })
+
+    render(<RouterProvider router={router} />)
+
+    const postsLink = await screen.findByRole('link', { name: 'Go to posts' })
+
+    fireEvent.click(postsLink)
+
+    const fromPostsLink = await screen.findByRole('link', {
+      name: 'From posts',
+    })
+
+    expect(fromPostsLink).toBeInTheDocument()
+
+    const toInvoicesLink = await screen.findByRole('link', {
+      name: 'To invoices',
+    })
+
+    fireEvent.click(toInvoicesLink)
+
+    const fromInvoicesLink = await screen.findByRole('link', {
+      name: 'From invoices',
+    })
+
+    expect(fromInvoicesLink).toBeInTheDocument()
+
+    expect(fromPostsLink).not.toBeInTheDocument()
+
+    fireEvent.click(await screen.findByRole('link', { name: 'To posts' }))
+
+    expect(await screen.findByText('On Posts')).toBeInTheDocument()
+
+    expect(fromInvoicesLink).not.toBeInTheDocument()
+
+    expect(ErrorComponent).not.toHaveBeenCalled()
+  })
 })
 
 describe('createLink', () => {
diff --git a/packages/router-vite-plugin/tests/snapshots/createServerFn.tsx b/packages/router-vite-plugin/tests/snapshots/createServerFn.tsx
index 4a7970951..b627e11fb 100644
--- a/packages/router-vite-plugin/tests/snapshots/createServerFn.tsx
+++ b/packages/router-vite-plugin/tests/snapshots/createServerFn.tsx
@@ -1,4 +1,5 @@
 import { createServerFn } from '@tanstack/start';
+import { z } from 'zod';
 export const withUseServer = createServerFn('GET', async function () {
   "use server";
 
@@ -13,9 +14,27 @@ export const withoutUseServer = createServerFn('GET', async () => {
   await new Promise(r => setTimeout(r, 500));
   return axios.get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10));
 });
-export const withVariable = createServerFn('GET', abstractedFunction);
+export const withVariable = createServerFn('GET', (...args: Parameters<Parameters<typeof createServerFn>[1]>) => {
+  "use server";
+
+  return abstractedFunction.apply(this, args);
+});
 async function abstractedFunction() {
   console.log('Fetching posts...');
   await new Promise(r => setTimeout(r, 500));
   return axios.get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10));
-}
\ No newline at end of file
+}
+function zodValidator<TSchema extends z.ZodSchema, TResult>(schema: TSchema, fn: (input: z.output<TSchema>) => TResult) {
+  return async (input: unknown) => {
+    return fn(schema.parse(input));
+  };
+}
+export const withZodValidator = createServerFn('GET', (...args: Parameters<Parameters<typeof createServerFn>[1]>) => {
+  "use server";
+
+  return zodValidator(z.number(), input => {
+    return {
+      'you gave': input
+    };
+  }).apply(this, args);
+});
\ No newline at end of file
diff --git a/packages/router-vite-plugin/tests/snapshots/createServerFn@split.tsx b/packages/router-vite-plugin/tests/snapshots/createServerFn@split.tsx
index 4471984e3..aa8092af8 100644
--- a/packages/router-vite-plugin/tests/snapshots/createServerFn@split.tsx
+++ b/packages/router-vite-plugin/tests/snapshots/createServerFn@split.tsx
@@ -1,4 +1,5 @@
 import { createServerFn } from '@tanstack/start'
+import { z } from 'zod'
 
 export const withUseServer = createServerFn('GET', async function () {
   console.log('Fetching posts...')
@@ -25,3 +26,19 @@ async function abstractedFunction() {
     .get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts')
     .then((r) => r.data.slice(0, 10))
 }
+
+function zodValidator<TSchema extends z.ZodSchema, TResult>(
+  schema: TSchema,
+  fn: (input: z.output<TSchema>) => TResult,
+) {
+  return async (input: unknown) => {
+    return fn(schema.parse(input))
+  }
+}
+
+export const withZodValidator = createServerFn(
+  'GET',
+  zodValidator(z.number(), (input) => {
+    return { 'you gave': input }
+  }),
+)
diff --git a/packages/router-vite-plugin/tests/test-files/createServerFn.tsx b/packages/router-vite-plugin/tests/test-files/createServerFn.tsx
index 4471984e3..aa8092af8 100644
--- a/packages/router-vite-plugin/tests/test-files/createServerFn.tsx
+++ b/packages/router-vite-plugin/tests/test-files/createServerFn.tsx
@@ -1,4 +1,5 @@
 import { createServerFn } from '@tanstack/start'
+import { z } from 'zod'
 
 export const withUseServer = createServerFn('GET', async function () {
   console.log('Fetching posts...')
@@ -25,3 +26,19 @@ async function abstractedFunction() {
     .get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts')
     .then((r) => r.data.slice(0, 10))
 }
+
+function zodValidator<TSchema extends z.ZodSchema, TResult>(
+  schema: TSchema,
+  fn: (input: z.output<TSchema>) => TResult,
+) {
+  return async (input: unknown) => {
+    return fn(schema.parse(input))
+  }
+}
+
+export const withZodValidator = createServerFn(
+  'GET',
+  zodValidator(z.number(), (input) => {
+    return { 'you gave': input }
+  }),
+)