BH3D Logo
TanStack Router Data Loading

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:

  1. beforeLoad functions run sequentially from outermost to innermost route
  2. 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

TanStack Router beforeLoad Loader flow

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

  1. Use beforeLoad for:

    • Authentication/authorization checks
    • Shared data needed by multiple nested routes
  2. Use loader for:

    • Route-specific data
    • Heavy data fetching operations
    • Operations that can run in parallel
  3. Keep beforeLoad performant:

    • Minimize network requests and heavy processing
    • Consider caching for expensive operations
    • Remember that slow beforeLoad functions block everything downstream
  4. 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.