server-functions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServer Functions
服务端函数
Server functions are type-safe RPCs created with . They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions.
createServerFnCRITICAL: Server functions are API endpoints. They are reachable independently of whichever route renders the calling UI. Auth must be enforced inside the handler (or via middleware) for any server function that touches private data. Routeis UX, not the data boundary. See start-core/auth-server-primitives for the session/middleware pattern. CRITICAL: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go insidebeforeLoad, NOT in loaders directly. CRITICAL: Do not usecreateServerFndirectives,"use server", or any Next.js/Remix server patterns. TanStack Start usesgetServerSidePropsexclusively.createServerFn
服务端函数是使用 创建的类型安全 RPC。它们仅在服务端运行,但可以从任意位置调用——加载器、组件、钩子、事件处理器或其他服务端函数。
createServerFn关键警告:服务端函数是 API 端点。无论哪个路由渲染了调用它们的 UI,它们都可以被独立访问。对于任何涉及私有数据的服务端函数,必须在处理器内部(或通过中间件)强制进行身份验证。 路由的是 UX 层面的,而非数据边界。会话/中间件模式请参考 start-core/auth-server-primitives。 关键警告:加载器是同构的——它们会在客户端和服务端同时运行。数据库查询、文件系统访问和机密 API 密钥必须放在beforeLoad内部,不能直接写在加载器中。 关键警告:请勿使用createServerFn指令、"use server"或任何 Next.js/Remix 的服务端模式。TanStack Start 仅使用getServerSideProps。createServerFn
Basic Usage
基础用法
tsx
import { createServerFn } from '@tanstack/react-start'
// GET (default)
const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
// POST
const saveData = createServerFn({ method: 'POST' }).handler(async () => {
return { success: true }
})tsx
import { createServerFn } from '@tanstack/react-start'
// GET (default)
const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
// POST
const saveData = createServerFn({ method: 'POST' }).handler(async () => {
return { success: true }
})Calling from Loaders
从加载器中调用
tsx
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
component: PostList,
})
function PostList() {
const { posts } = Route.useLoaderData()
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}tsx
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
component: PostList,
})
function PostList() {
const { posts } = Route.useLoaderData()
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}Calling from Components
从组件中调用
Use the hook to call server functions from event handlers:
useServerFntsx
import { useServerFn } from '@tanstack/react-start'
const deletePost = createServerFn({ method: 'POST' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
await db.delete('posts').where({ id: data.id })
return { success: true }
})
function DeleteButton({ postId }: { postId: string }) {
const deletePostFn = useServerFn(deletePost)
return (
<button onClick={() => deletePostFn({ data: { id: postId } })}>
Delete
</button>
)
}使用 钩子从事件处理器中调用服务端函数:
useServerFntsx
import { useServerFn } from '@tanstack/react-start'
const deletePost = createServerFn({ method: 'POST' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
await db.delete('posts').where({ id: data.id })
return { success: true }
})
function DeleteButton({ postId }: { postId: string }) {
const deletePostFn = useServerFn(deletePost)
return (
<button onClick={() => deletePostFn({ data: { id: postId } })}>
Delete
</button>
)
}Cache-Coherent Mutations
缓存一致的变更操作
Keep reads and writes behind server functions that use the same authoritative store. Await the write, then invalidate the route cache so its loader reads the persisted result:
tsx
const listIssues = createServerFn({ method: 'GET' }).handler(() => {
return db.issues.findMany()
})
const createIssue = createServerFn({ method: 'POST' })
.validator((data: unknown) => {
if (
typeof data !== 'object' ||
data === null ||
!('title' in data) ||
typeof data.title !== 'string' ||
data.title.trim().length === 0
) {
throw new Error('Title is required')
}
return { title: data.title.trim() }
})
.handler(async ({ data }) => {
return db.issues.create({ data })
})
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
component: IssuesPage,
})
function IssuesPage() {
const router = useRouter()
const createIssueFn = useServerFn(createIssue)
const handleCreate = async (title: string) => {
await createIssueFn({ data: { title } })
await router.invalidate({ sync: true })
}
// render Route.useLoaderData() and call handleCreate from the form
}Do not update only local component state after a persistent mutation. Verify the rendered list after create, update, delete, and a fresh page load.
将读写操作都封装在使用同一权威数据源的服务端函数之后。等待写入完成,然后使路由缓存失效,这样加载器就会读取持久化后的结果:
tsx
const listIssues = createServerFn({ method: 'GET' }).handler(() => {
return db.issues.findMany()
})
const createIssue = createServerFn({ method: 'POST' })
.validator((data: unknown) => {
if (
typeof data !== 'object' ||
data === null ||
!('title' in data) ||
typeof data.title !== 'string' ||
data.title.trim().length === 0
) {
throw new Error('Title is required')
}
return { title: data.title.trim() }
})
.handler(async ({ data }) => {
return db.issues.create({ data })
})
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
component: IssuesPage,
})
function IssuesPage() {
const router = useRouter()
const createIssueFn = useServerFn(createIssue)
const handleCreate = async (title: string) => {
await createIssueFn({ data: { title } })
await router.invalidate({ sync: true })
}
// render Route.useLoaderData() and call handleCreate from the form
}持久化变更后,请勿仅更新本地组件状态。在创建、更新、删除操作以及页面刷新后,都要验证渲染的列表是否正确。
Input Validation
输入验证
Basic Validator
基础验证器
tsx
const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })tsx
const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })Zod Validator
Zod 验证器
tsx
import { z } from 'zod'
const createUser = createServerFn({ method: 'POST' })
.validator(
z.object({
name: z.string().min(1),
age: z.number().min(0),
}),
)
.handler(async ({ data }) => {
return `Created user: ${data.name}, age ${data.age}`
})Input validation does not validate output serialization. When a response schema changes, update the database selection, service return value, server-function result, loader consumer, and UI. Add a runtime test that calls the handler or HTTP boundary and asserts the new field in the returned payload.
tsx
import { z } from 'zod'
const createUser = createServerFn({ method: 'POST' })
.validator(
z.object({
name: z.string().min(1),
age: z.number().min(0),
}),
)
.handler(async ({ data }) => {
return `Created user: ${data.name}, age ${data.age}`
})输入验证不会验证输出的序列化。当响应 schema 发生变化时,需要更新数据库查询、服务返回值、服务端函数结果、加载器消费者和 UI。请添加运行时测试,调用处理器或 HTTP 边界,并断言返回的 payload 中包含新字段。
FormData
FormData
tsx
const submitForm = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Expected FormData')
}
return {
name: data.get('name')?.toString() || '',
email: data.get('email')?.toString() || '',
}
})
.handler(async ({ data }) => {
return { success: true }
})tsx
const submitForm = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Expected FormData')
}
return {
name: data.get('name')?.toString() || '',
email: data.get('email')?.toString() || '',
}
})
.handler(async ({ data }) => {
return { success: true }
})Error Handling
错误处理
Errors
错误
tsx
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}tsx
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}Redirects
重定向
tsx
import { redirect } from '@tanstack/react-router'
const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})tsx
import { redirect } from '@tanstack/react-router'
const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})Not Found
未找到
tsx
import { notFound } from '@tanstack/react-router'
const getPost = createServerFn()
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})tsx
import { notFound } from '@tanstack/react-router'
const getPost = createServerFn()
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})Server Context Utilities
服务端上下文工具
Access request/response details inside server function handlers:
tsx
import { createServerFn } from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
// Public, non-personalized data — safe to cache shared across users.
const getPublicData = createServerFn({ method: 'GET' }).handler(async () => {
setResponseHeaders({
// 'public' is correct ONLY when the response does not depend on identity.
// For anything tied to a session/user/tenant, use 'private' or 'no-store'.
'Cache-Control': 'public, max-age=300',
})
setResponseStatus(200)
return fetchPublicData()
})
// Authenticated data — must NOT be 'public'.
const getMyData = createServerFn({ method: 'GET' }).handler(async () => {
const authHeader = getRequestHeader('Authorization')
// ... auth check ...
setResponseHeaders({
// 'private' = only the user-agent may cache. Vary by Cookie/Authorization
// so any intermediary that does cache keys by identity, not URL alone.
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return fetchPersonalizedData()
})Available utilities:
- — full Request object
getRequest() - — single request header
getRequestHeader(name) - — single response header
setResponseHeader(name, value) - — multiple response headers
setResponseHeaders(headers) - — HTTP status code
setResponseStatus(code)
在服务端函数处理器中访问请求/响应详情:
tsx
import { createServerFn } from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
// Public, non-personalized data — safe to cache shared across users.
const getPublicData = createServerFn({ method: 'GET' }).handler(async () => {
setResponseHeaders({
// 'public' is correct ONLY when the response does not depend on identity.
// For anything tied to a session/user/tenant, use 'private' or 'no-store'.
'Cache-Control': 'public, max-age=300',
})
setResponseStatus(200)
return fetchPublicData()
})
// Authenticated data — must NOT be 'public'.
const getMyData = createServerFn({ method: 'GET' }).handler(async () => {
const authHeader = getRequestHeader('Authorization')
// ... auth check ...
setResponseHeaders({
// 'private' = only the user-agent may cache. Vary by Cookie/Authorization
// so any intermediary that does cache keys by identity, not URL alone.
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return fetchPersonalizedData()
})可用工具:
- — 完整的 Request 对象
getRequest() - — 获取单个请求头
getRequestHeader(name) - — 设置单个响应头
setResponseHeader(name, value) - — 设置多个响应头
setResponseHeaders(headers) - — 设置 HTTP 状态码
setResponseStatus(code)
File Organization
文件组织
text
src/utils/
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)tsx
// users.server.ts — server-only helpers
import { db } from '~/db'
export async function findUserById(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) })
}tsx
// users.functions.ts — server functions
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
return findUserById(data.id)
})Static imports of server functions are safe — the build replaces implementations with RPC stubs in client bundles.
text
src/utils/
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)tsx
// users.server.ts — server-only helpers
import { db } from '~/db'
export async function findUserById(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) })
}tsx
// users.functions.ts — server functions
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
return findUserById(data.id)
})静态导入服务端函数是安全的——构建过程会在客户端包中将实现替换为 RPC 存根。
Common Mistakes
常见错误
1. CRITICAL: Relying on a route guard to protect a server function
1. 严重:依赖路由守卫保护服务端函数
A redirect protects the route's UI, not the data endpoint. exposes a callable endpoint that an attacker can hit directly — no need to load the route at all. Auth on the endpoint is the security boundary; auth on the route is UX.
beforeLoadcreateServerFntsx
// WRONG — the route guard doesn't reach the handler
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
return db.orders.findMany() // ← anyone can call the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
loader: () => getMyOrders(),
})
// CORRECT — auth enforced on the handler itself
const getMyOrders = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return db.orders.findMany({ where: { userId: context.session.userId } })
})Apply (or an equivalent in-handler check) to every that needs auth. See start-core/auth-server-primitives for the full session/middleware pattern and start-core/middleware for composing the factory.
authMiddlewarecreateServerFnbeforeLoadcreateServerFntsx
// WRONG — the route guard doesn't reach the handler
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
return db.orders.findMany() // ← anyone can call the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
loader: () => getMyOrders(),
})
// CORRECT — auth enforced on the handler itself
const getMyOrders = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return db.orders.findMany({ where: { userId: context.session.userId } })
})请为每个需要身份验证的 应用 (或等效的处理器内检查)。完整的会话/中间件模式请参考 start-core/auth-server-primitives,中间件工厂的组合方式请参考 start-core/middleware。
createServerFnauthMiddleware2. CRITICAL: Putting server-only code in loaders
2. 严重:将仅服务端代码放在加载器中
tsx
// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
},
})
// CORRECT — use createServerFn for server-only logic
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})tsx
// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
},
})
// CORRECT — use createServerFn for server-only logic
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})3. CRITICAL: Using Next.js / Remix / React Router DOM patterns
3. 严重:使用 Next.js / Remix / React Router DOM 模式
If the file lives at , , , or imports anything from or , it is wrong-framework code. TanStack Start uses + + .
src/pages/app/layout.tsx_app/react-router-domnext/src/routes/createFileRoutecreateServerFntsx
// WRONG — "use server" is a React directive, not used in TanStack Start
'use server'
export async function getUser() { ... }
// WRONG — getServerSideProps is Next.js Pages Router
export async function getServerSideProps() { ... }
// WRONG — Next.js App Router server component data fetching
export default async function Page() {
const data = await fetch(...).then(r => r.json())
return <div>{data}</div>
}
// WRONG — Remix
export async function loader({ request }) { ... }
export async function action({ request }) { ... }
// WRONG — react-router-dom (a different library)
import { Link, useNavigate } from 'react-router-dom'
// CORRECT — TanStack Start
import { createServerFn } from '@tanstack/react-start'
import { Link, useNavigate, createFileRoute } from '@tanstack/react-router'
const getUser = createServerFn({ method: 'GET' })
.handler(async () => { ... })
export const Route = createFileRoute('/users/$id')({
loader: ({ params }) => getUser({ data: { id: params.id } }),
component: UserPage,
})If you see , , or in agent output, the agent is generating for the wrong framework. Build will fail or routes will conflict at runtime.
src/pages/app/layout.tsxreact-router-dom如果文件位于 、、 目录下,或者导入了 或 中的任何内容,那都是错误框架的代码。TanStack Start 使用 + + 。
src/pages/app/layout.tsx_app/react-router-domnext/src/routes/createFileRoutecreateServerFntsx
// WRONG — "use server" is a React directive, not used in TanStack Start
'use server'
export async function getUser() { ... }
// WRONG — getServerSideProps is Next.js Pages Router
export async function getServerSideProps() { ... }
// WRONG — Next.js App Router server component data fetching
export default async function Page() {
const data = await fetch(...).then(r => r.json())
return <div>{data}</div>
}
// WRONG — Remix
export async function loader({ request }) { ... }
export async function action({ request }) { ... }
// WRONG — react-router-dom (a different library)
import { Link, useNavigate } from 'react-router-dom'
// CORRECT — TanStack Start
import { createServerFn } from '@tanstack/react-start'
import { Link, useNavigate, createFileRoute } from '@tanstack/react-router'
const getUser = createServerFn({ method: 'GET' })
.handler(async () => { ... })
export const Route = createFileRoute('/users/$id')({
loader: ({ params }) => getUser({ data: { id: params.id } }),
component: UserPage,
})如果在 Agent 输出中看到 、 或 ,说明 Agent 生成的是错误框架的代码。构建会失败,或者运行时会出现路由冲突。
src/pages/app/layout.tsxreact-router-dom4. HIGH: Dynamic imports for server functions
4. 高危:动态导入服务端函数
tsx
// WRONG — can cause bundler issues
const { getUser } = await import('~/utils/users.functions')
// CORRECT — static imports are safe, build handles environment shaking
import { getUser } from '~/utils/users.functions'tsx
// WRONG — can cause bundler issues
const { getUser } = await import('~/utils/users.functions')
// CORRECT — static imports are safe, build handles environment shaking
import { getUser } from '~/utils/users.functions'5. HIGH: Awaiting server function without calling it
5. 高危:等待服务端函数但未调用它
createServerFn()tsx
// WRONG — getItems is a function, not a Promise
const data = await getItems
// CORRECT — call the function
const data = await getItems()
// With validated input
const data = await getItems({ data: { id: '1' } })createServerFn()tsx
// WRONG — getItems is a function, not a Promise
const data = await getItems
// CORRECT — call the function
const data = await getItems()
// With validated input
const data = await getItems({ data: { id: '1' } })6. CRITICAL: Caching authenticated responses with Cache-Control: public
Cache-Control: public6. 严重:使用 Cache-Control: public
缓存已认证响应
Cache-Control: publicCache-Control: public, max-age=Ntsx
// WRONG — auth'd response, public cache, leaks to next user via CDN
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession() // identity-dependent
setResponseHeaders({ 'Cache-Control': 'public, max-age=300' })
return db.orders.findMany({ where: { userId: session.userId } })
})
// CORRECT — private + Vary so any cache that does store it keys by identity
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession()
setResponseHeaders({
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return db.orders.findMany({ where: { userId: session.userId } })
})
// ALSO CORRECT — opt out entirely for sensitive data
setResponseHeaders({ 'Cache-Control': 'no-store' })Rule of thumb: if the handler reads a session/cookie/auth header or branches on identity, the response is not . Default to (or for sensitive data); reach for only on responses that are byte-for-byte identical regardless of who asks. See also start-core/deployment for ISR/Cache-Control on full pages.
publicprivateno-storepublicCache-Control: public, max-age=Ntsx
// WRONG — auth'd response, public cache, leaks to next user via CDN
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession() // identity-dependent
setResponseHeaders({ 'Cache-Control': 'public, max-age=300' })
return db.orders.findMany({ where: { userId: session.userId } })
})
// CORRECT — private + Vary so any cache that does store it keys by identity
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession()
setResponseHeaders({
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return db.orders.findMany({ where: { userId: session.userId } })
})
// ALSO CORRECT — opt out entirely for sensitive data
setResponseHeaders({ 'Cache-Control': 'no-store' })经验法则:如果处理器读取了会话/cookie/认证头,或者根据身份进行了分支处理,那么响应就不是 的。默认使用 (敏感数据则用 );只有当响应对任何请求者都完全相同时,才使用 。整页的 ISR/Cache-Control 配置请参考 start-core/deployment。
publicprivateno-storepublic7. MEDIUM: When to wrap with useServerFn
useServerFn7. 中危:何时使用 useServerFn
包装
useServerFnuseServerFnthrow redirect()throw notFound()useMutationuseQuerytsx
// Plain data — direct call is fine (also fine to pass to useMutation/useQuery)
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
useMutation({ mutationFn: deletePost })
// Throws redirect/notFound — MUST wrap with useServerFn so the router handles the throw
const signupFn = useServerFn(signup) // signup throws redirect on success
<button onClick={() => signupFn({ data: form })}>Sign up</button>If in doubt: wrap with . It's a no-op for plain-data functions and the safe default when a function might later add a redirect.
useServerFn当服务端函数使用 或 时,必须使用 ——这个钩子会将抛出的内容连接到路由,让重定向真正生效。对于仅返回数据的服务端函数(可以直接调用,或者通过 / 调用),这个钩子是可选的。
throw redirect()throw notFound()useServerFnuseMutationuseQuerytsx
// Plain data — direct call is fine (also fine to pass to useMutation/useQuery)
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
useMutation({ mutationFn: deletePost })
// Throws redirect/notFound — MUST wrap with useServerFn so the router handles the throw
const signupFn = useServerFn(signup) // signup throws redirect on success
<button onClick={() => signupFn({ data: form })}>Sign up</button>如果有疑问:就用 包装。对于纯数据函数来说它是空操作,而当函数后续可能添加重定向时,这是更安全的默认选择。
useServerFn8. CRITICAL: Self-fetching a relative API URL from a loader
8. 严重:在加载器中自行请求相对 API URL
tsx
// WRONG — this loader also runs during SSR, where a relative URL may fail
export const Route = createFileRoute('/issues')({
loader: () => fetch('/api/issues').then((response) => response.json()),
})
// CORRECT — call the server function; the client build gets an RPC stub
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
})Relative is fine in a browser-only event handler. It is not a universal loader data strategy.
fetchtsx
// WRONG — this loader also runs during SSR, where a relative URL may fail
export const Route = createFileRoute('/issues')({
loader: () => fetch('/api/issues').then((response) => response.json()),
})
// CORRECT — call the server function; the client build gets an RPC stub
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
})相对路径的 在仅浏览器端的事件处理器中是没问题的,但它不是通用的加载器数据获取策略。
fetchCross-References
相关参考
- start-core/execution-model — understanding where code runs
- start-core/middleware — composing server functions with middleware
- start-core/auth-server-primitives — sessions, cookies, OAuth, CSRF, rate limiting (the server-side half of auth; /
getCurrentUser-style helpers belong here, not at module scope)useSession - router-core/auth-and-guards — routing-side UX guards; data auth belongs in the server function, server route, or API endpoint handler/middleware
- start-core/execution-model — 理解代码的运行位置
- start-core/middleware — 使用中间件组合服务端函数
- start-core/auth-server-primitives — 会话、Cookie、OAuth、CSRF、限流(身份验证的服务端部分;/
getCurrentUser类的辅助函数应该放在这里,而不是模块作用域)useSession - router-core/auth-and-guards — 路由层面的 UX 守卫;数据身份验证应该放在服务端函数、服务端路由或 API 端点处理器/中间件中