Data loading is a common concern for web applications and is extremely related to routing. When loading any page for your app, it's ideal if all of the async requirements for those routes are fetched and fulfilled as early as possible and in parallel. The router is the best place to coordinate all of these async dependencies as it's usually the only place in your app that knows about where users are headed before content is rendered.
You may be familiar with getServerSideProps
from Next.js or or loaders
from Remix/React-Router. Both of these APIs assumes that the router will store and manage your data. This approach is great for use cases covered by both of those libraries, but TanStack Router is designed to function a bit differently than you're used to. Let's dig in!
Most routers that support data fetching will store the data for you in memory on the client. This is fine, but puts a large responsibility and stress on the router to handle many cross-cutting and complex challenges that come with managing server-data, client-side caches and mutations.
Instead of storing your data, TanStack Router is designed to coordinate your data fetching. This means that you can use any data fetching library you want, and the router will coordinate the fetching of your data in a way that aligns with your users' navigation.
Any data fetching library that supports asynchronous dependencies can be used with TanStack Router. This includes:
Or, even...
Literally any library that can return a promise and read/write data is supported.
Just because TanStack Router works with any data-fetching library doesn't mean we'd leave you empty handed! You may not always need all of the features of the above libraries (or you may not want to pay their cost in bundle size). This is why we created TanStack Loaders!
For the following examples, we'll show you the basics of data loading using TanStack Loaders, but as we've already mentioned, these same principles can be applied to any state management library worth it's salt. Let's get started!
loader
route optionThe loader
route option is a function that is called every time a route is matched and loaded for:
Let's repeat that again. Every time someone navigates to a new route, refreshes the current route, or preloads a route, the matching routes' onload
functions will be called.
⚠️ If you've used Remix or Next.js, you may be used to the idea that data loading only happens for routes on the page that change when navigating. eg. If you were to navigate from
/posts
to/posts/1
, theloader
/getServerSideProps
function for/posts
would not be called again. This is not the case with TanStack Router. Every route'sloader
function will be called every time a route is loaded.
The biggest reason for calling loader
every time is to notify your data loading library that data is or will be required. How your data loading library uses that information may vary, but obviously, this pattern is hopeful that your data fetching library can cache and refetch in the background. If you're using TanStack Loaders or TanStack Query, this is the default behavior.
Here is a simple example of using loader
to fetch data for a route:
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postsLoader = new Loader({ key: 'posts', loader: async (params) => { const res = await fetch(`/api/posts`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', async loader() { // Wait for the loader to finish await postsLoader.load() }, component: () => { // Access the loader's data, in this case with the useLoader hook const posts = useLoader({ loader: postsLoader })
return <div>...</div> },})
loader
ParametersThe loader
function receives a single parameter, which is an object with the following properties:
params
- The route's parsed path paramssearch
- The route's search query, parsed, validated and typed including inherited search params from parent routesrouteSearch
- The route's search query, parsed, validated and typed excluding inherited search params from parent routeshash
- The route's hashcontext
- The route's context object including inherited context from parent routesrouteContext
- The route's context object, excluding inherited context from parent routessignal
- The route's abort signal which is cancelled when the route is unloaded or when the loader
call becomes outdated.Using these parameters, we can do a lot of cool things. Let's take a look at a few examples
The params
property of the loader
function is an object containing the route's path params.
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postLoader = new Loader({ key: 'post', // Accept a postId string variable loader: async (postId: string) => { const res = await fetch(`/api/posts/${postId}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const postRoute = new Route({ getParentPath: () => postsRoute, path: '$postId', async loader({ params }) { await postLoader.load({ variables: params.postId }) }, component: () => { const { postId } = useParams({ from: postRoute.id }) const posts = useLoader({ loader: postLoader, variables: postId })
return <div>...</div> },})
The search
and routeSearch
properties of the loader
function are objects containing the route's search params. search
contains all of the search params including parent search params. routeSearch
only includes specific search params from this route. In this example, we'll use zod to validate and parse the search params for /posts/$postId
route and use them in an onload
function and our component.
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postsLoader = new Loader({ key: 'posts', // Accept a page number variable loader: async (pageIndex: number) => { const res = await fetch(`/api/posts?page=${pageIndex}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', validateSearch: z.object({ pageIndex: z.number().int().nonnegative().catch(0), }), async loader({ search }) { await postsLoader.load({ variables: search.pageIndex }) }, component: () => { const search = useSearchParams({ from: postsRoute.id }) const posts = useLoader({ loader: postsLoader, variables: search.pageIndex, })
return <div>...</div> },})
The context
and routeContext
properties of the loader
function are objects containing the route's context. context
is the context object for the route including context from parent routes. routeContext
is the context object for the route excluding context from parent routes. In this example, we'll create a loaderClient
and inject it into our router's context. We'll then use that client in our loader
function and our component.
🧠 Context is a powerful tool for dependency injection. You can use it to inject services, loaders, and other objects into your router and routes. You can also additively pass data down the route tree at every route using a route's
getContext
option.
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postsLoader = new Loader({ key: 'posts', loader: async () => { const res = await fetch(`/api/posts`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const loaderClient = new LoaderClient({ getLoaders: () => [postsLoader],})
// Use RootRoute's special `withRouterContext` method to require a specific type// of router context to be both available in every route and to be passed to// the router for implementation.
const rootRoute = RootRoute.withRouterContext<{ loaderClient: typeof loaderClient}>()()
// Notice how our postsRoute reference context to get the loader client// This can be a powerful tool for dependency injection across your router// and routes.const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', async loader({ context }) { await context.loaderClient.getLoader({ key: 'posts' }).load() }, component: () => { const posts = useLoader({ key: 'posts' })
return <div>...</div> },})
const routeTree = rootRoute.addChildren([postsRoute])
const router = new Router({ routeTree, context: { // Supply our loaderClient to the whole router loaderClient, },})
The signal
property of the loader
function is an AbortSignal which is cancelled when the route is unloaded or when the loader
call becomes outdated. This is useful for cancelling network requests when the route is unloaded or when the route's params change. Here is an example using TanStack Loader's signal passthrough:
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postsLoader = new Loader({ key: 'posts', // Accept a page number variable loader: async (pageIndex: number, { signal }) => { const res = await fetch(`/api/posts?page=${pageIndex}`, { signal }) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', async loader({ signal }) { // Pass the route's signal to the loader await postsLoader.load({ signal }) }, component: () => { const posts = useLoader({ loader: postsLoader })
return <div>...</div> },})
prefetch
flagThe prefetch
property of the loader
function is a boolean which is true
when the route is being loaded via a prefetch action. Some data loading libraries may handle prefetching differently than a standard fetch, so you may want to pass prefetch
to your data loading library, or use it to execute the appropriate data loading logic. Here is an example using TanStack Loader and it's built-in prefetch
flag:
import { Route } from '@tanstack/router'import { Loader, useLoader } from '@tanstack/react-loaders'
const postsLoader = new Loader({ key: 'posts', loader: async () => { const res = await fetch(`/api/posts?page=${pageIndex}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() },})
const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', async loader({ prefetch }) { // Pass the route's prefetch to the loader await postsLoader.load({ prefetch }) }, component: () => { const posts = useLoader({ loader: postsLoader })
return <div>...</div> },})
🧠 TanStack Loaders uses the
prefetch
flag to determine cache freshness vs non-prefetch calls and also to determine if the globalisLoading
orisPrefetching
flags should be incremented or not.
There's plenty more to learn about TanStack Loaders (and Actions!). If you plan on using them with TanStack Router, it's highly recommended that you read through their documentation: