execution-model
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExecution Model
执行模型
Understanding where code runs is fundamental to TanStack Start. This skill covers the isomorphic execution model and how to control environment boundaries.
CRITICAL: ALL code in TanStack Start is isomorphic by default — it runs in BOTH server and client bundles. Route loaders run on BOTH server (during SSR) AND client (during navigation). Server-only operations MUST use. CRITICAL: Module-levelcreateServerFnaccess is wrong on two axes — security (values leak into the client bundle) AND runtime correctness (on Cloudflare Workers and other edge runtimes, env is injected per-request, so module-level reads evaluate toprocess.enveven on the server). Read env insideundefinedor another per-request function, never at module scope. CRITICAL:.handler()prefixed environment variables are exposed to the client bundle. Server secrets must NOT have theVITE_prefix. CRITICAL: Relative URLs belong to browser-only code. An isomorphic loader also runs during SSR, whereVITE_may have no base URL. Call a server function from the loader, or keep the fetch inside an explicit environment boundary.fetch('/api/...')
理解代码的运行位置是 TanStack Start 的基础。本技能介绍同构执行模型以及如何控制环境边界。
关键警告:TanStack Start 中的所有代码默认都是同构的——它会同时打包到服务端和客户端 bundle 中。路由加载器会在服务端(SSR 期间)和客户端(导航期间)都运行。仅服务端执行的操作必须使用。 关键警告:模块级别的createServerFn访问在两个层面上都是错误的——安全性(值会泄露到客户端 bundle)和运行时正确性(在 Cloudflare Workers 及其他边缘运行时中,环境变量是按请求注入的,因此即使在服务端,模块级读取的结果也是process.env)。请在undefined或其他按请求执行的函数中读取环境变量,绝对不要在模块作用域中读取。 关键警告:带有.handler()前缀的环境变量会暴露给客户端 bundle。服务端密钥绝对不能使用VITE_前缀。 关键警告:相对 URL 属于仅浏览器端代码的范畴。同构加载器在 SSR 期间也会运行,此时VITE_可能没有基础 URL。请从加载器中调用服务端函数,或将 fetch 放在明确的环境边界内。fetch('/api/...')
Execution Control APIs
执行控制 API
| API | Use Case | Client Behavior | Server Behavior |
|---|---|---|---|
| RPC calls, data mutations | Network request to server | Direct execution |
| Utility functions | Throws error | Direct execution |
| Browser utilities | Direct execution | Throws error |
| Different impl per env | Uses | Uses |
| Browser-only components | Renders children | Renders fallback |
| Hydration-dependent logic | | |
| Mark whole file server-only | Import denied | Allowed |
| Mark whole file client-only | Allowed | Import denied |
| API | 使用场景 | 客户端行为 | 服务端行为 |
|---|---|---|---|
| RPC 调用、数据变更 | 向服务端发起网络请求 | 直接执行 |
| 工具函数 | 抛出错误 | 直接执行 |
| 浏览器工具函数 | 直接执行 | 抛出错误 |
| 不同环境有不同实现 | 使用 | 使用 |
| 仅浏览器端组件 | 渲染子组件 | 渲染降级内容 |
| 依赖水合的逻辑 | 水合后为 | |
| 标记整个文件为仅服务端 | 导入被拒绝 | 允许 |
| 标记整个文件为仅客户端 | 允许 | 导入被拒绝 |
Server-Only Execution
仅服务端执行
createServerFn (RPC pattern)
createServerFn(RPC 模式)
The primary way to run server-only code. On the client, calls become fetch requests:
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'
const fetchUser = createServerFn().handler(async () => {
const secret = process.env.API_SECRET // safe — server only
return await db.users.find()
})
// Client calls this via network request
const user = await fetchUser()运行仅服务端代码的主要方式。在客户端,调用会转化为 fetch 请求:
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'
const fetchUser = createServerFn().handler(async () => {
const secret = process.env.API_SECRET // safe — server only
return await db.users.find()
})
// Client calls this via network request
const user = await fetchUser()createServerOnlyFn (throws on client)
createServerOnlyFn(客户端调用会抛出错误)
For utility functions that must never run on client:
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerOnlyFn } from '@tanstack/react-start'
const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL)
// Server: returns the value
// Client: THROWS an error用于绝对不能在客户端运行的工具函数:
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerOnlyFn } from '@tanstack/react-start'
const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL)
// Server: returns the value
// Client: THROWS an errorClient-Only Execution
仅客户端执行
createClientOnlyFn
createClientOnlyFn
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createClientOnlyFn } from '@tanstack/react-start'
const saveToStorage = createClientOnlyFn((key: string, value: string) => {
localStorage.setItem(key, value)
})tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createClientOnlyFn } from '@tanstack/react-start'
const saveToStorage = createClientOnlyFn((key: string, value: string) => {
localStorage.setItem(key, value)
})ClientOnly Component
ClientOnly 组件
tsx
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { ClientOnly } from '@tanstack/react-router'
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}tsx
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { ClientOnly } from '@tanstack/react-router'
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}useHydrated Hook
useHydrated 钩子
tsx
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { useHydrated } from '@tanstack/react-router'
function TimeZoneDisplay() {
const hydrated = useHydrated()
const timeZone = hydrated
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'UTC'
return <div>Your timezone: {timeZone}</div>
}Behavior: SSR → , first client render → , after hydration → (stays ).
falsefalsetruetruetsx
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { useHydrated } from '@tanstack/react-router'
function TimeZoneDisplay() {
const hydrated = useHydrated()
const timeZone = hydrated
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'UTC'
return <div>Your timezone: {timeZone}</div>
}行为:SSR 时为 ,客户端首次渲染时为 ,水合完成后为 (并保持 )。
falsefalsetruetrueEnvironment-Specific Implementations
环境特定实现
tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createIsomorphicFn } from '@tanstack/react-start'
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))tsx
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createIsomorphicFn } from '@tanstack/react-start'
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))Import Protection: File Markers
导入保护:文件标记
Experimental.
The and filename suffixes (e.g. ) opt a file into Start's import protection — it can't be imported from the wrong environment. When you can't or don't want to rename the file, add a side-effect import at the top of the file to apply the same protection by marker:
.server.*.client.*db.server.tsts
// src/lib/secrets.ts (filename can't be *.server.ts)
import '@tanstack/react-start/server-only'
// (or @tanstack/solid-start/server-only, @tanstack/vue-start/server-only)
export function getApiKey() {
return process.env.API_KEY
}ts
// src/lib/storage.ts
import '@tanstack/react-start/client-only'
// (or @tanstack/solid-start/client-only, @tanstack/vue-start/client-only)
export function savePreferences(prefs: Record<string, string>) {
localStorage.setItem('prefs', JSON.stringify(prefs))
}Rules:
- Both markers in the same file is an error.
- Type-only imports are ignored (they erase to nothing at runtime).
- Default behavior is in production builds and
errorin dev. The mock returns a recursive Proxy so dev keeps running while you fix the import graph.mock
Pick the right tool:
- File should never run on the wrong side and has no client API → filename or
*.server.ts.import '@tanstack/<fw>-start/server-only' - One symbol needs to behave differently per environment → .
createIsomorphicFn().client(...).server(...) - One function should error if called from the wrong side → /
createServerOnlyFn.createClientOnlyFn - Component renders only after hydration → or
<ClientOnly>.useHydrated()
实验性功能。
.server.*.client.*db.server.tsts
// src/lib/secrets.ts (filename can't be *.server.ts)
import '@tanstack/react-start/server-only'
// (or @tanstack/solid-start/server-only, @tanstack/vue-start/server-only)
export function getApiKey() {
return process.env.API_KEY
}ts
// src/lib/storage.ts
import '@tanstack/react-start/client-only'
// (or @tanstack/solid-start/client-only, @tanstack/vue-start/client-only)
export function savePreferences(prefs: Record<string, string>) {
localStorage.setItem('prefs', JSON.stringify(prefs))
}规则:
- 同一文件中同时使用两个标记会报错。
- 仅类型导入会被忽略(它们在运行时会被完全擦除)。
- 默认行为在生产构建中为 (报错),在开发环境中为
error(模拟)。模拟模式会返回一个递归 Proxy,这样在你修复导入关系的过程中,开发环境仍能正常运行。mock
选择合适的工具:
- 文件绝对不能在错误的一端运行且没有客户端 API → 使用 文件名或
*.server.ts。import '@tanstack/<fw>-start/server-only' - 某个标识符需要在不同环境中有不同行为 → 使用 。
createIsomorphicFn().client(...).server(...) - 某个函数如果从错误的一端调用应该报错 → 使用 /
createServerOnlyFn。createClientOnlyFn - 组件仅在水合后渲染 → 使用 或
<ClientOnly>。useHydrated()
Environment Variables
环境变量
Server-Side (inside createServerFn)
服务端(createServerFn 内部)
Access any variable via :
process.envtsx
const connectDb = createServerFn().handler(async () => {
const url = process.env.DATABASE_URL // no prefix needed
return createConnection(url)
})通过 访问任意变量:
process.envtsx
const connectDb = createServerFn().handler(async () => {
const url = process.env.DATABASE_URL // no prefix needed
return createConnection(url)
})Client-Side (components)
客户端(组件中)
Only prefixed variables are available:
VITE_tsx
// Framework-specific component type (React.ReactNode, JSX.Element, etc.)
function ApiProvider({ children }: { children: React.ReactNode }) {
const apiUrl = import.meta.env.VITE_API_URL // available
// import.meta.env.DATABASE_URL → undefined (security)
return (
<ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider>
)
}只有带 前缀的变量可用:
VITE_tsx
// Framework-specific component type (React.ReactNode, JSX.Element, etc.)
function ApiProvider({ children }: { children: React.ReactNode }) {
const apiUrl = import.meta.env.VITE_API_URL // available
// import.meta.env.DATABASE_URL → undefined (security)
return (
<ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider>
)
}Runtime Client Variables
运行时客户端变量
If you need server-side variables on the client without prefix, pass them through a server function:
VITE_tsx
const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
return process.env.MY_RUNTIME_VAR
})
export const Route = createFileRoute('/')({
loader: async () => {
const foo = await getRuntimeVar()
return { foo }
},
component: () => {
const { foo } = Route.useLoaderData()
return <div>{foo}</div>
},
})如果你需要在客户端使用不带 前缀的服务端变量,可以通过服务端函数传递:
VITE_tsx
const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
return process.env.MY_RUNTIME_VAR
})
export const Route = createFileRoute('/')({
loader: async () => {
const foo = await getRuntimeVar()
return { foo }
},
component: () => {
const { foo } = Route.useLoaderData()
return <div>{foo}</div>
},
})Type Safety for Environment Variables
环境变量的类型安全
tsx
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly DATABASE_URL: string
readonly JWT_SECRET: string
}
}
}
export {}tsx
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly DATABASE_URL: string
readonly JWT_SECRET: string
}
}
}
export {}Common Mistakes
常见错误
1. CRITICAL: Assuming loaders are server-only
1. 关键警告:误以为加载器仅在服务端运行
tsx
// WRONG — loader runs on BOTH server and client
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const secret = process.env.API_SECRET // LEAKED to client
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
},
})
// CORRECT — use createServerFn
const getData = createServerFn({ method: 'GET' }).handler(async () => {
const secret = process.env.API_SECRET
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
})
export const Route = createFileRoute('/dashboard')({
loader: () => getData(),
})tsx
// WRONG — loader runs on BOTH server and client
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const secret = process.env.API_SECRET // LEAKED to client
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
},
})
// CORRECT — use createServerFn
const getData = createServerFn({ method: 'GET' }).handler(async () => {
const secret = process.env.API_SECRET
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
})
export const Route = createFileRoute('/dashboard')({
loader: () => getData(),
})2. CRITICAL: Reading process.env at module scope
2. 关键警告:在模块作用域读取 process.env
Module-level reads are wrong for two reasons, not one:
process.env- Security: the value can be inlined into the client bundle, leaking secrets.
- Runtime correctness (edge runtimes): Cloudflare Workers and other edge SSR runtimes inject env at request time. Module-level code runs at module load, before the env exists, so the read evaluates to even on the server. The bug only surfaces at deploy time.
undefined
tsx
// WRONG — leaks to client AND is undefined on Workers
const apiKey = process.env.SECRET_KEY
export function fetchData() {
/* uses apiKey, which is undefined under Worker SSR */
}
// CORRECT — read per-request, inside the handler
const fetchData = createServerFn({ method: 'GET' }).handler(async () => {
const apiKey = process.env.SECRET_KEY
return fetch(url, { headers: { Authorization: apiKey } })
})The same rule applies to middleware callbacks, server-route handlers, and any function that runs per request — read env there, not at the top of the file.
.server()模块级别的 读取错误的原因有两个,而不是一个:
process.env- 安全性:值可能被内联到客户端 bundle 中,导致密钥泄露。
- 运行时正确性(边缘运行时):Cloudflare Workers 和其他边缘 SSR 运行时会在请求时注入环境变量。模块级代码在模块加载时运行,此时环境变量还不存在,因此即使在服务端,读取的结果也是 。这个 bug 只会在部署时才会暴露。
undefined
tsx
// WRONG — leaks to client AND is undefined on Workers
const apiKey = process.env.SECRET_KEY
export function fetchData() {
/* uses apiKey, which is undefined under Worker SSR */
}
// CORRECT — read per-request, inside the handler
const fetchData = createServerFn({ method: 'GET' }).handler(async () => {
const apiKey = process.env.SECRET_KEY
return fetch(url, { headers: { Authorization: apiKey } })
})同样的规则适用于中间件 回调、服务端路由处理器以及任何按请求运行的函数——在这些函数中读取环境变量,不要在文件顶部读取。
.server()3. CRITICAL: Using VITE_ prefix for server secrets
3. 关键警告:给服务端密钥使用 VITE_ 前缀
bash
undefinedbash
undefinedWRONG — exposed to client bundle
WRONG — exposed to client bundle
VITE_SECRET_API_KEY=sk_live_xxx
VITE_SECRET_API_KEY=sk_live_xxx
CORRECT — no prefix for server secrets
CORRECT — no prefix for server secrets
SECRET_API_KEY=sk_live_xxx
SECRET_API_KEY=sk_live_xxx
CORRECT — VITE_ only for public client values
CORRECT — VITE_ only for public client values
VITE_APP_NAME=My App
undefinedVITE_APP_NAME=My App
undefined4. HIGH: Hydration mismatches
4. 高风险:水合不匹配
tsx
// WRONG — different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// CORRECT — consistent rendering
function CurrentTime() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <div>{time || 'Loading...'}</div>
}tsx
// WRONG — different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// CORRECT — consistent rendering
function CurrentTime() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <div>{time || 'Loading...'}</div>
}Architecture Decision Framework
架构决策框架
Server-Only ( / ):
createServerFncreateServerOnlyFn- Sensitive data (env vars, secrets)
- Database connections, file system
- External API keys
Client-Only ( / ):
createClientOnlyFn<ClientOnly>- DOM manipulation, browser APIs
- localStorage, geolocation
- Analytics/tracking
Isomorphic (default / ):
createIsomorphicFn- Data formatting, business logic
- Shared utilities
- Route loaders (they're isomorphic by nature)
仅服务端( / ):
createServerFncreateServerOnlyFn- 敏感数据(环境变量、密钥)
- 数据库连接、文件系统
- 外部 API 密钥
仅客户端( / ):
createClientOnlyFn<ClientOnly>- DOM 操作、浏览器 API
- localStorage、地理定位
- 分析/追踪
同构(默认 / ):
createIsomorphicFn- 数据格式化、业务逻辑
- 共享工具函数
- 路由加载器(它们本质上是同构的)
Cross-References
交叉参考
- start-core/server-functions — the primary server boundary
- start-core/deployment — deployment target affects execution
- start-core/server-functions —— 主要的服务端边界
- start-core/deployment —— 部署目标会影响执行