TanStack Start Loaders Explained
TanStack Start gives you two functions for loading data in routes: beforeLoad and loader. Both are isomorphic, meaning they run on both the server and the client depending on how users enter your app. Understanding the differences between them, and when each one runs, is essential for building TanStack Start applications effectively.
Most of what's covered here applies to TanStack Router as well. The server-side behavior is what TanStack Start adds on top.
Two Loaders, Two Purposes
beforeLoad: Sequential and Contextual
The beforeLoad function runs sequentially from the outermost parent route down to the deepest nested child. This ordering makes it ideal for route guards like authentication and authorization checks.
// routes/_authenticated.tsx
import { getUser } from '~/lib/auth'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async () => {
const user = await getUser()
if (!user) {
throw redirect({ to: '/login' })
}
return { user }
},
})
getUseris a hypothetical function, typically a server function, that fetches the user from the database or cookies, etc.
Whatever you return from beforeLoad merges into the router context. This data becomes available to:
- All child routes downstream (in their
beforeLoadandloaderfunctions) - Other route functions like
loaderandheadon the current route
// routes/_authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
beforeLoad: ({ context }) => {
// context.user is available here from the parent's beforeLoad
console.log(context.user.email)
},
})
loader: Parallel Execution
The loader function works differently. Once all beforeLoad functions have completed, the loader functions for all active routes in the route tree run in parallel. This parallel execution means faster page loads since data fetching happens concurrently.
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts()
return { posts }
},
})
Unlike beforeLoad, data returned from loader does not merge into the router context. Instead, it becomes the route's loader data, accessible only within that specific route.
When and Where Code Runs
First Load: Server-Side
When a user first enters your app (direct navigation, page refresh, or external link), by default, both beforeLoad and loader run on the server. The data is serialized and sent to the client along with the rendered HTML.
Subsequent Navigation: Client-Side
After the initial load, TanStack Start behaves more like a traditional SPA (Single Page Application). When users navigate between routes using client-side links, the loader functions run in the browser. Your server is not involved in these navigations.
This is why both functions must be isomorphic. The same code needs to work in both environments.
Data Loading Strategies
TanStack Start provides options to control this behavior per route. You can opt in or out of SSR for specific routes using the ssr option:
export const Route = createFileRoute('/client-only')({
ssr: false, // This route renders only on the client
loader: () => fetchData(),
})
There's also a "data-only" mode where loader functions run server-side but the route itself doesn't render on the server. This gives you server-side data fetching without full SSR rendering.
Caching Considerations
One important behavior to understand: beforeLoad and loader functions are invoked for all active routes in the current route tree, not just the route being navigated to. This happens whenever:
- A route reloads (via
router.invalidate()or stale time expiration) - Prerendering is enabled
- Any navigation occurs that involves those routes
This means if you have nested routes like /_authenticated/dashboard/settings, navigating within that tree will re-run the loaders for all three route segments.
For this reason, it's recommended to use a caching layer like TanStack Query for data fetching within loaders. This prevents unnecessary network requests when loaders re-execute:
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// Using TanStack Query's ensureQueryData prevents redundant fetches
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
})
Without caching, every navigation or reload would trigger fresh API calls, even when the data hasn't changed.
The Serialization Requirement
Here's a crucial constraint: whatever you return from beforeLoad or loader must be serializable. The data travels from server to client, so it needs to survive that journey.
This means you cannot return:
- Functions
- Class instances
- Symbols
- Values typed as
unknown
TanStack Start's type system helps enforce this. You'll get type errors if you try to return something that isn't serializable. This is soft enforcement at the type level, but it catches most mistakes before they become runtime errors.
Promises and Streaming
One powerful exception: Promises are serializable. TanStack Start can stream Promise resolutions from server to client, enabling patterns like deferred data loading:
export const Route = createFileRoute('/posts')({
loader: () => ({
// Critical data - awaited before render
posts: await fetchPosts(),
// Non-critical data - streams in later
recommendations: fetchRecommendations(),
}),
})
There are also a few other data types that are automatically serialized. See the router documentation for more details.
Return Type Inference
You rarely (never?) need to specify return types explicitly. TanStack Start is excellent at inferring types from your loader functions. The inferred types flow through to your components automatically.
Custom Serialization
If you need to serialize custom types, TanStack Router supports configuring your own serialization logic. This is an advanced escape hatch for special cases.
Accessing Context and Loader Data
Accessing Route Context
In components, use the useRouteContext hook:
function Dashboard() {
const { user } = Route.useRouteContext()
return <div>Welcome, {user.email}</div>
}
In child routes, access parent context through the route's context parameter:
export const Route = createFileRoute('/_authenticated/settings')({
beforeLoad: ({ context }) => {
// Access user from parent route's beforeLoad
console.log(context.user.id)
},
})
Accessing Loader Data
For loader data, use the useLoaderData hook:
function PostsPage() {
const { posts } = Route.useLoaderData()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
The loader data is scoped to the route. Child routes cannot access a parent's loader data directly (use context via beforeLoad for shared data).
Type Safety Tips
Property Order Matters
TanStack Router uses TypeScript generics that depend on the order in which you define route properties. Defining properties in the wrong order can cause type inference to break.
The correct order is:
params(validation)validateSearchloaderDepsbeforeLoadloadercomponent(and other rendering options)
ESLint Plugin
Rather than memorizing this order, use the official ESLint plugin. It automatically enforces the correct property order:
npm install -D @tanstack/eslint-plugin-router
// eslint.config.js
import pluginRouter from '@tanstack/eslint-plugin-router'
export default [
...pluginRouter.configs['flat/recommended'],
]
The plugin catches ordering issues and other common mistakes before they cause confusing type errors.
Summary
The key points to remember:
beforeLoadruns sequentially (parent to child) and is ideal for route guards. Returned data merges into router context.loaderruns in parallel across all active routes. Returned data is route-specific.- Both functions are isomorphic: they run on the server for initial loads and on the client for SPA navigations.
- Return values must be serializable. Promises are allowed and can be streamed.
- The type system helps enforce serialization constraints.
- Use the ESLint plugin to ensure correct property ordering for proper type inference.
Understanding these fundamentals will help you structure your data loading effectively and avoid common pitfalls when building TanStack Start applications.
Share this article
Enjoyed this article?
Subscribe to our newsletter for more insights and updates.