File: execution-model.md | Updated: 11/15/2025
Search...
+ K
Auto
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide
Documentation
Framework
React
Version
Latest
Search...
+ K
Menu
Getting Started
Guides
Examples
Tutorials
Framework
React
Version
Latest
Menu
Getting Started
Guides
Examples
Tutorials
On this page
Copy Markdown
Understanding where code runs is fundamental to building TanStack Start applications. This guide explains TanStack Start's execution model and how to control where your code executes.
Core Principle: Isomorphic by Default
-------------------------------------
All code in TanStack Start is isomorphic by default - it runs and is included in both server and client bundles unless explicitly constrained.
tsx
// ✅ This runs on BOTH server and client
function formatPrice(price: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price)
}
// ✅ Route loaders are ISOMORPHIC
export const Route = createFileRoute('/products')({
loader: async () => {
// This runs on server during SSR AND on client during navigation
const response = await fetch('/api/products')
return response.json()
},
})
// ✅ This runs on BOTH server and client
function formatPrice(price: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price)
}
// ✅ Route loaders are ISOMORPHIC
export const Route = createFileRoute('/products')({
loader: async () => {
// This runs on server during SSR AND on client during navigation
const response = await fetch('/api/products')
return response.json()
},
})
Critical Understanding: Route loaders are isomorphic - they run on both server and client, not just the server.
The Execution Boundary
----------------------
TanStack Start applications run in two environments:
Execution Control APIs
----------------------
### Server-Only Execution
| API | Use Case | Client Behavior | | --- | --- | --- | | createServerFn() | RPC calls, data mutations | Network request to server | | createServerOnlyFn(fn) | Utility functions | Throws error |
tsx
import { createServerFn, createServerOnlyFn } from '@tanstack/react-start'
// RPC: Server execution, callable from client
const updateUser = createServerFn({ method: 'POST' })
.inputValidator((data: UserData) => data)
.handler(async ({ data }) => {
// Only runs on server, but client can call it
return await db.users.update(data)
})
// Utility: Server-only, client crashes if called
const getEnvVar = createServerOnlyFn(() => process.env.DATABASE_URL)
import { createServerFn, createServerOnlyFn } from '@tanstack/react-start'
// RPC: Server execution, callable from client
const updateUser = createServerFn({ method: 'POST' })
.inputValidator((data: UserData) => data)
.handler(async ({ data }) => {
// Only runs on server, but client can call it
return await db.users.update(data)
})
// Utility: Server-only, client crashes if called
const getEnvVar = createServerOnlyFn(() => process.env.DATABASE_URL)
| API | Use Case | Server Behavior | | --- | --- | --- | | createClientOnlyFn(fn) | Browser utilities | Throws error | | <ClientOnly> | Components needing browser APIs | Renders fallback |
tsx
import { createClientOnlyFn } from '@tanstack/react-start'
import { ClientOnly } from '@tanstack/react-router'
// Utility: Client-only, server crashes if called
const saveToStorage = createClientOnlyFn((key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value))
})
// Component: Only renders children after hydration
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}
import { createClientOnlyFn } from '@tanstack/react-start'
import { ClientOnly } from '@tanstack/react-router'
// Utility: Client-only, server crashes if called
const saveToStorage = createClientOnlyFn((key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value))
})
// Component: Only renders children after hydration
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}
For more granular control over hydration-dependent behavior, use the useHydrated hook. It returns a boolean indicating whether the client has been hydrated:
tsx
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>
}
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:
This is useful when you need to conditionally render content based on client-side data (like browser timezone, locale, or localStorage) while providing a sensible fallback for server rendering.
### Environment-Specific Implementations
tsx
import { createIsomorphicFn } from '@tanstack/react-start'
// Different implementation per environment
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
import { createIsomorphicFn } from '@tanstack/react-start'
// Different implementation per environment
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
Architectural Patterns
----------------------
### Progressive Enhancement
Build components that work without JavaScript and enhance with client-side functionality:
tsx
function SearchForm() {
const [query, setQuery] = useState('')
return (
<form action="/search" method="get">
<input
name="q"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ClientOnly fallback={<button type="submit">Search</button>}>
<SearchButton onSearch={() => search(query)} />
</ClientOnly>
</form>
)
}
function SearchForm() {
const [query, setQuery] = useState('')
return (
<form action="/search" method="get">
<input
name="q"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ClientOnly fallback={<button type="submit">Search</button>}>
<SearchButton onSearch={() => search(query)} />
</ClientOnly>
</form>
)
}
tsx
const storage = createIsomorphicFn()
.server((key: string) => {
// Server: File-based cache
const fs = require('node:fs')
return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key]
})
.client((key: string) => {
// Client: localStorage
return JSON.parse(localStorage.getItem(key) || 'null')
})
const storage = createIsomorphicFn()
.server((key: string) => {
// Server: File-based cache
const fs = require('node:fs')
return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key]
})
.client((key: string) => {
// Client: localStorage
return JSON.parse(localStorage.getItem(key) || 'null')
})
### RPC vs Direct Function Calls
Understanding when to use server functions vs server-only functions:
tsx
// createServerFn: RPC pattern - server execution, client callable
const fetchUser = createServerFn().handler(async () => await db.users.find())
// Usage from client component:
const user = await fetchUser() // ✅ Network request
// createServerOnlyFn: Crashes if called from client
const getSecret = createServerOnlyFn(() => process.env.SECRET)
// Usage from client:
const secret = getSecret() // ❌ Throws error
// createServerFn: RPC pattern - server execution, client callable
const fetchUser = createServerFn().handler(async () => await db.users.find())
// Usage from client component:
const user = await fetchUser() // ✅ Network request
// createServerOnlyFn: Crashes if called from client
const getSecret = createServerOnlyFn(() => process.env.SECRET)
// Usage from client:
const secret = getSecret() // ❌ Throws error
Common Anti-Patterns
--------------------
### Environment Variable Exposure
tsx
// ❌ Exposes to client bundle
const apiKey = process.env.SECRET_KEY
// ✅ Server-only access
const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY)
// ❌ Exposes to client bundle
const apiKey = process.env.SECRET_KEY
// ✅ Server-only access
const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY)
### Incorrect Loader Assumptions
tsx
// ❌ Assuming loader is server-only
export const Route = createFileRoute('/users')({
loader: () => {
// This runs on BOTH server and client!
const secret = process.env.SECRET // Exposed to client
return fetch(`/api/users?key=${secret}`)
},
})
// ✅ Use server function for server-only operations
const getUsersSecurely = createServerFn().handler(() => {
const secret = process.env.SECRET // Server-only
return fetch(`/api/users?key=${secret}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsersSecurely(), // Isomorphic call to server function
})
// ❌ Assuming loader is server-only
export const Route = createFileRoute('/users')({
loader: () => {
// This runs on BOTH server and client!
const secret = process.env.SECRET // Exposed to client
return fetch(`/api/users?key=${secret}`)
},
})
// ✅ Use server function for server-only operations
const getUsersSecurely = createServerFn().handler(() => {
const secret = process.env.SECRET // Server-only
return fetch(`/api/users?key=${secret}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsersSecurely(), // Isomorphic call to server function
})
tsx
// ❌ Different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// ✅ Consistent rendering
function CurrentTime() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <div>{time || 'Loading...'}</div>
}
// ❌ Different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// ✅ Consistent rendering
function CurrentTime() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <div>{time || 'Loading...'}</div>
}
Manual vs API-Driven Environment Detection
------------------------------------------
tsx
// Manual: You handle the logic
function logMessage(msg: string) {
if (typeof window === 'undefined') {
console.log(`[SERVER]: ${msg}`)
} else {
console.log(`[CLIENT]: ${msg}`)
}
}
// API: Framework handles it
const logMessage = createIsomorphicFn()
.server((msg) => console.log(`[SERVER]: ${msg}`))
.client((msg) => console.log(`[CLIENT]: ${msg}`))
// Manual: You handle the logic
function logMessage(msg: string) {
if (typeof window === 'undefined') {
console.log(`[SERVER]: ${msg}`)
} else {
console.log(`[CLIENT]: ${msg}`)
}
}
// API: Framework handles it
const logMessage = createIsomorphicFn()
.server((msg) => console.log(`[SERVER]: ${msg}`))
.client((msg) => console.log(`[CLIENT]: ${msg}`))
Architecture Decision Framework
-------------------------------
Choose Server-Only when:
Choose Client-Only when:
Choose Isomorphic when:
Security Considerations
-----------------------
### Bundle Analysis
Always verify server-only code isn't included in client bundles:
bash
# Analyze client bundle
npm run build
# Check dist/client for any server-only imports
# Analyze client bundle
npm run build
# Check dist/client for any server-only imports
### Environment Variable Strategy
Handle server/client execution errors gracefully:
tsx
function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundaryComponent
fallback={<div>Something went wrong</div>}
onError={(error) => {
if (typeof window === 'undefined') {
console.error('[SERVER ERROR]:', error)
} else {
console.error('[CLIENT ERROR]:', error)
}
}}
>
{children}
</ErrorBoundaryComponent>
)
}
function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundaryComponent
fallback={<div>Something went wrong</div>}
onError={(error) => {
if (typeof window === 'undefined') {
console.error('[SERVER ERROR]:', error)
} else {
console.error('[CLIENT ERROR]:', error)
}
}}
>
{children}
</ErrorBoundaryComponent>
)
}
Understanding TanStack Start's execution model is crucial for building secure, performant, and maintainable applications. The isomorphic-by-default approach provides flexibility while the execution control APIs give you precise control when needed.
