Understanding TanStack Router's beforeLoad and Loader Behavior
By Ben Houston, 2025-04-07
TanStack Router provides powerful data loading capabilities through its beforeLoad
and loader
functions. Understanding their behavior is critical for effective use of TanStack Router in your projects.
Execution Environment
Both beforeLoad
and loader
are designed, when using TanStack Router, to run on the client. If you are running a combined with TanStack Router + Start you need to design these functions to run on both the client and the server. This means you need to only use JavaScript that isn't dependent on the browser or the Node.js, but rather can work in both. If using TanStack/Start and you want to use functions that run on Node.js / on the server, such as database or file system, you should use TanStack/Start's server function capabilities.
Order of Execution
The execution order is key to understanding data flow in TanStack Router:
- beforeLoad functions run sequentially from outermost to innermost route
- loader functions run in parallel, but only after ALL beforeLoad functions complete
This execution pattern explains several important behaviors:
- Parent route data is available to child routes
- All loaders can access beforeLoad data
- Loaders cannot access data from other loaders
Data Access and Flow
beforeLoad Data
- Each
beforeLoad
receives:{ params, search, context }
context
contains merged results from all parent routes' beforeLoad functions- Each beforeLoad's return value is merged into this context
- This creates a data flow from parent to child routes
loader Data
- Each
loader
receives:{ params, context }
- To access
{ search }
, you need to set the loaderDeps explicitly context
made available to loader contains the complete merged result from all beforeLoad functions- Each loader's return is independent and isolated from other route's loaders
- Loader results are NOT merged or shared between their routes
Accessing Data in Components
export const Route = createFileRoute('/example')({ component: () => { // Access merged beforeLoad context (includes parent route data) const routeContext = Route.useRouteContext(); // Access only this route's loader data const loaderData = Route.useLoaderData(); return <YourComponent />; } });
Complete Example
const testServerFn = createServerFn({ method: 'GET' }) .validator((data: unknown) => data as string) .handler(async ({ data }) => { return 'Received: ' + data; }); export const Route = createFileRoute('/index')({ component: () => { // Access beforeLoad results (includes parent data) const routeContext = Route.useRouteContext(); // Will output: { customData: 'hello world from beforeLoad!' } // Access only this route's loader data const loaderData = Route.useLoaderData(); // Will output: { customData: 'hello world from loader!' } return <div>Route Loaders Test</div>; }, beforeLoad: async ({ context, params, search }) => { // Access parent beforeLoad data const parentData = context; // Server functions work in beforeLoad const serverResponse = await testServerFn('beforeLoad'); // Merged with parent data and available to child routes return { customData: 'hello world from beforeLoad!' }; }, loader: async ({ context, params }) => { // Access ALL beforeLoad data const beforeLoadData = context; // Server functions work in loader too const serverResponse = await testServerFn('loader'); // Independent of other loaders return { customData: 'hello world from loader!' }; } });
Performance Implications
The execution order creates important performance considerations:
beforeLoad
- Runs sequentially - a slow function blocks everything downstream
- A slow parent route
beforeLoad
delays ALL child routes - Best for critical, lightweight operations needed by multiple nested routes
loader
- Runs in parallel - doesn't block other loaders
- A slow
loader
only affects its specific nested route - Ideal for heavier, route-specific data fetching
Handling Nested Routes
To prevent property collisions in merged beforeLoad data, use namespaced objects:
// Parent route beforeLoad: async () => { return { user: { id: 123, name: 'John' } }; }; // Child route beforeLoad: async ({ context }) => { return { settings: { theme: 'dark' } // Won't conflict with parent's 'user' property }; };
Best Practices
-
Use beforeLoad for:
- Authentication/authorization checks
- Shared data needed by multiple nested routes
-
Use loader for:
- Route-specific data
- Heavy data fetching operations
- Operations that can run in parallel
-
Keep beforeLoad performant:
- Minimize network requests and heavy processing
- Consider caching for expensive operations
- Remember that slow beforeLoad functions block everything downstream
-
Structure beforeLoad returns carefully:
- Use namespaced objects to prevent property collisions
- Consider TypeScript interfaces for type safety
By understanding these patterns and following these practices, you'll be well on your way to mastering TanStack Router's data loading capabilities.