execution-model

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Execution 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
createServerFn
. CRITICAL: Module-level
process.env
access 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 to
undefined
even on the server). Read env inside
.handler()
or another per-request function, never at module scope. CRITICAL:
VITE_
prefixed environment variables are exposed to the client bundle. Server secrets must NOT have the
VITE_
prefix. CRITICAL: Relative URLs belong to browser-only code. An isomorphic loader also runs during SSR, where
fetch('/api/...')
may have no base URL. Call a server function from the loader, or keep the fetch inside an explicit environment boundary.
理解代码的运行位置是 TanStack Start 的基础。本技能介绍同构执行模型以及如何控制环境边界。
关键警告:TanStack Start 中的所有代码默认都是同构的——它会同时打包到服务端和客户端 bundle 中。路由加载器会在服务端(SSR 期间)和客户端(导航期间)都运行。仅服务端执行的操作必须使用
createServerFn
。 关键警告:模块级别的
process.env
访问在两个层面上都是错误的——安全性(值会泄露到客户端 bundle)和运行时正确性(在 Cloudflare Workers 及其他边缘运行时中,环境变量是按请求注入的,因此即使在服务端,模块级读取的结果也是
undefined
)。请在
.handler()
或其他按请求执行的函数中读取环境变量,绝对不要在模块作用域中读取。 关键警告:带有
VITE_
前缀的环境变量会暴露给客户端 bundle。服务端密钥绝对不能使用
VITE_
前缀。 关键警告:相对 URL 属于仅浏览器端代码的范畴。同构加载器在 SSR 期间也会运行,此时
fetch('/api/...')
可能没有基础 URL。请从加载器中调用服务端函数,或将 fetch 放在明确的环境边界内。

Execution Control APIs

执行控制 API

APIUse CaseClient BehaviorServer Behavior
createServerFn()
RPC calls, data mutationsNetwork request to serverDirect execution
createServerOnlyFn(fn)
Utility functionsThrows errorDirect execution
createClientOnlyFn(fn)
Browser utilitiesDirect executionThrows error
createIsomorphicFn()
Different impl per envUses
.client()
impl
Uses
.server()
impl
<ClientOnly>
Browser-only componentsRenders childrenRenders fallback
useHydrated()
Hydration-dependent logic
true
after hydration
false
import '@tanstack/<fw>-start/server-only'
Mark whole file server-onlyImport deniedAllowed
import '@tanstack/<fw>-start/client-only'
Mark whole file client-onlyAllowedImport denied
API使用场景客户端行为服务端行为
createServerFn()
RPC 调用、数据变更向服务端发起网络请求直接执行
createServerOnlyFn(fn)
工具函数抛出错误直接执行
createClientOnlyFn(fn)
浏览器工具函数直接执行抛出错误
createIsomorphicFn()
不同环境有不同实现使用
.client()
实现
使用
.server()
实现
<ClientOnly>
仅浏览器端组件渲染子组件渲染降级内容
useHydrated()
依赖水合的逻辑水合后为
true
false
import '@tanstack/<fw>-start/server-only'
标记整个文件为仅服务端导入被拒绝允许
import '@tanstack/<fw>-start/client-only'
标记整个文件为仅客户端允许导入被拒绝

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 error

Client-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 →
false
, first client render →
false
, after hydration →
true
(stays
true
).
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>
}
行为:SSR 时为
false
,客户端首次渲染时为
false
,水合完成后为
true
(并保持
true
)。

Environment-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
.server.*
and
.client.*
filename suffixes (e.g.
db.server.ts
) 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:
ts
// 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
    error
    in production builds and
    mock
    in dev. The mock returns a recursive Proxy so dev keeps running while you fix the import graph.
Pick the right tool:
  • File should never run on the wrong side and has no client API →
    *.server.ts
    filename or
    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 →
    <ClientOnly>
    or
    useHydrated()
    .
实验性功能。
.server.*
和
.client.*
文件名后缀(例如
db.server.ts
)会让文件加入 Start 的导入保护机制——它无法被错误的环境导入。如果你不能或不想重命名文件,可以在文件顶部添加一个副作用导入,通过标记来应用相同的保护:
ts
// 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
    (报错),在开发环境中为
    mock
    (模拟)。模拟模式会返回一个递归 Proxy,这样在你修复导入关系的过程中,开发环境仍能正常运行。
选择合适的工具:
  • 文件绝对不能在错误的一端运行且没有客户端 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.env
:
tsx
const connectDb = createServerFn().handler(async () => {
  const url = process.env.DATABASE_URL // no prefix needed
  return createConnection(url)
})
通过
process.env
访问任意变量:
tsx
const connectDb = createServerFn().handler(async () => {
  const url = process.env.DATABASE_URL // no prefix needed
  return createConnection(url)
})

Client-Side (components)

客户端(组件中)

Only
VITE_
prefixed variables are available:
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
VITE_
prefix, pass them through a server function:
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
process.env
reads are wrong for two reasons, not one:
  1. Security: the value can be inlined into the client bundle, leaking secrets.
  2. 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
    undefined
    even on the server. The bug only surfaces at deploy time.
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
.server()
callbacks, server-route handlers, and any function that runs per request — read env there, not at the top of the file.
模块级别的
process.env
读取错误的原因有两个,而不是一个:
  1. 安全性:值可能被内联到客户端 bundle 中,导致密钥泄露。
  2. 运行时正确性(边缘运行时):Cloudflare Workers 和其他边缘 SSR 运行时会在请求时注入环境变量。模块级代码在模块加载时运行,此时环境变量还不存在,因此即使在服务端,读取的结果也是
    undefined
    。这个 bug 只会在部署时才会暴露。
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
undefined
bash
undefined

WRONG — 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
undefined
VITE_APP_NAME=My App
undefined

4. 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 (
createServerFn
/
createServerOnlyFn
):
  • 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)
仅服务端(
createServerFn
/
createServerOnlyFn
):
  • 敏感数据(环境变量、密钥)
  • 数据库连接、文件系统
  • 外部 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 —— 部署目标会影响执行