export default `# Authenticated Routes
Authentication is an extremely common requirement for web applications. In this guide, we'll walk through how to use TanStack Router to build protected routes, and how to redirect users to login if they try to access them.
## The \`route.beforeLoad\` Option
The \`route.beforeLoad\` option allows you to specify a function that will be called before a route is loaded. It receives all of the same arguments that the \`route.loader\` function does. This is a great place to check if a user is authenticated, and redirect them to a login page if they are not.
The \`beforeLoad\` function runs in relative order to these other route loading functions:
- Route Matching (Top-Down)
- \`route.params.parse\`
- \`route.validateSearch\`
- Route Loading (including Preloading)
- **\`route.beforeLoad\`**
- \`route.onError\`
- Route Loading (Parallel)
- \`route.component.preload?\`
- \`route.load\`
**It's important to know that the \`beforeLoad\` function for a route is called _before any of its child routes' \`beforeLoad\` functions_.** It is essentially a middleware function for the route and all of its children.
**If you throw an error in \`beforeLoad\`, none of its children will attempt to load**.
## Redirecting
While not required, some authentication flows require redirecting to a login page. To do this, you can **throw a \`redirect()\`** from \`beforeLoad\`:
\`\`\`tsx
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
if (!isAuthenticated()) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use \`router.state.resolvedLocation\` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}
},
})
\`\`\`
> [!TIP]
> The \`redirect()\` function takes all of the same options as the \`navigate\` function, so you can pass options like \`replace: true\` if you want to replace the current history entry instead of adding a new one.
### Handling Auth Check Failures
If your authentication check can throw errors (network failures, token validation, etc.), wrap it in try/catch:
# React
\`\`\`tsx
import { createFileRoute, redirect, isRedirect } from '@tanstack/react-router'
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
try {
const user = await verifySession() // might throw on network error
if (!user) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
return { user }
} catch (error) {
// Re-throw redirects (they're intentional, not errors)
if (isRedirect(error)) throw error
// Auth check failed (network error, etc.) - redirect to login
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
\`\`\`
# Solid
\`\`\`tsx
import { createFileRoute, redirect, isRedirect } from '@tanstack/solid-router'
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
try {
const user = await verifySession() // might throw on network error
if (!user) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
return { user }
} catch (error) {
// Re-throw redirects (they're intentional, not errors)
if (isRedirect(error)) throw error
// Auth check failed (network error, etc.) - redirect to login
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
\`\`\`
The [\`isRedirect()\`](../api/router/isRedirectFunction.md) helper distinguishes between actual errors and intentional redirects.
Once you have authenticated a user, it's also common practice to redirect them back to the page they were trying to access. To do this, you can utilize the \`redirect\` search param that we added in our original redirect. Since we'll be replacing the entire URL with what it was, \`router.history.push\` is better suited for this than \`router.navigate\`:
\`\`\`tsx
router.history.push(search.redirect)
\`\`\`
## Non-Redirected Authentication
Some applications choose to not redirect users to a login page, and instead keep the user on the same page and show a login form that either replaces the main content or hides it via a modal. This is also possible with TanStack Router by simply short circuiting rendering the \`\` that would normally render the child routes:
\`\`\`tsx
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
component: () => {
if (!isAuthenticated()) {
return
}
return
},
})
\`\`\`
This keeps the user on the same page, but still allows you to render a login form. Once the user is authenticated, you can simply render the \`\` and the child routes will be rendered.
## Authentication using React context/hooks
If your authentication flow relies on interactions with React context and/or hooks, you'll need to pass down your authentication state to TanStack Router using \`router.context\` option.
> [!IMPORTANT]
> React hooks are not meant to be consumed outside of React components. If you need to use a hook outside of a React component, you need to extract the returned state from the hook in a component that wraps your \`\` and then pass the returned value down to TanStack Router.
We'll cover the \`router.context\` options in-detail in the [Router Context](./router-context.md) section.
Here's an example that uses React context and hooks for protecting authenticated routes in TanStack Router. See the entire working setup in the [Authenticated Routes example](https://github.com/TanStack/router/tree/main/examples/react/authenticated-routes).
# React
\`\`\`tsx title="src/routes/__root.tsx"
import { createRootRouteWithContext } from '@tanstack/react-router'
interface MyRouterContext {
// The ReturnType of your useAuth hook or the value of your AuthContext
auth: AuthState
}
export const Route = createRootRouteWithContext()({
component: () => ,
})
\`\`\`
\`\`\`tsx title="src/router.tsx"
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
context: {
// auth will initially be undefined
// We'll be passing down the auth state from within a React component
auth: undefined!,
},
})
\`\`\`
\`\`\`tsx title="src/App.tsx"
import { RouterProvider } from '@tanstack/react-router'
import { AuthProvider, useAuth } from './auth'
import { router } from './router'
function InnerApp() {
const auth = useAuth()
return
}
function App() {
return (
)
}
\`\`\`
# Solid
\`\`\`tsx title="src/routes/__root.tsx"
import { createRootRouteWithContext } from '@tanstack/solid-router'
interface MyRouterContext {
// The ReturnType of your useAuth hook or the value of your AuthContext
auth: AuthState
}
export const Route = createRootRouteWithContext()({
component: () => ,
})
\`\`\`
\`\`\`tsx title="src/router.tsx"
import { createRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
context: {
// auth will initially be undefined
// We'll be passing down the auth state from within a React component
auth: undefined!,
},
})
\`\`\`
\`\`\`tsx title="src/App.tsx"
import { RouterProvider } from '@tanstack/solid-router'
import { AuthProvider, useAuth } from './auth'
import { router } from './router'
function InnerApp() {
const auth = useAuth()
return
}
function App() {
return (
)
}
\`\`\`
Then in the authenticated route, you can check the auth state using the \`beforeLoad\` function, and **throw a \`redirect()\`** to your **Login route** if the user is not signed-in.
# React
\`\`\`tsx title="src/routes/dashboard.route.tsx"
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
})
\`\`\`
# Solid
\`\`\`tsx title="src/routes/dashboard.route.tsx"
import { createFileRoute, redirect } from '@tanstack/solid-router'
export const Route = createFileRoute('/dashboard')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated()) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
})
\`\`\`
You can _optionally_, also use the [Non-Redirected Authentication](#non-redirected-authentication) approach to show a login form instead of calling a **redirect**.
This approach can also be used in conjunction with Pathless or Layout Route to protect all routes under their parent route.
## Related How-To Guides
For detailed, step-by-step implementation guides, see:
- [How to Set Up Basic Authentication](../how-to/setup-authentication.md) - Complete setup with React Context and protected routes
- [How to Integrate Authentication Providers](../how-to/setup-auth-providers.md) - Use Auth0, Clerk, or Supabase
- [How to Set Up Role-Based Access Control](../how-to/setup-rbac.md) - Implement permissions and role-based routing
## Examples
Working authentication examples are available in the repository:
- [Basic Authentication Example](https://github.com/TanStack/router/tree/main/examples/react/authenticated-routes) - Simple authentication with context
- [Firebase Authentication](https://github.com/TanStack/router/tree/main/examples/react/authenticated-routes-firebase) - Firebase Auth integration
- [TanStack Start Auth Examples](https://github.com/TanStack/router/tree/main/examples/react) - Various auth implementations with TanStack Start
# Automatic Code Splitting
The automatic code splitting feature in TanStack Router allows you to optimize your application's bundle size by lazily loading route components and their associated data. This is particularly useful for large applications where you want to minimize the initial load time by only loading the necessary code for the current route.
To turn this feature on, simply set the \`autoCodeSplitting\` option to \`true\` in your bundler plugin configuration. This enables the router to automatically handle code splitting for your routes without requiring any additional setup.
\`\`\`ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true, // Enable automatic code splitting
}),
],
})
\`\`\`
But that's just the beginning! TanStack Router's automatic code splitting is not only easy to enable, but it also provides powerful customization options to tailor how your routes are split into chunks. This allows you to optimize your application's performance based on your specific needs and usage patterns.
## How does it work?
TanStack Router's automatic code splitting works by transforming your route files both during 'development' and at 'build' time. It rewrites the route definitions to use lazy-loading wrappers for components and loaders, which allows the bundler to group these properties into separate chunks.
> [!TIP]
> A **chunk** is a file that contains a portion of your application's code, which can be loaded on demand. This helps reduce the initial load time of your application by only loading the code that is needed for the current route.
So when your application loads, it doesn't include all the code for every route. Instead, it only includes the code for the routes that are initially needed. As users navigate through your application, additional chunks are loaded on demand.
This happens seamlessly, without requiring you to manually split your code or manage lazy loading. The TanStack Router bundler plugin takes care of everything, ensuring that your routes are optimized for performance right out of the box.
### The transformation process
When you enable automatic code splitting, the bundler plugin does this by using static code analysis look at your the code in your route files to transform them into optimized outputs.
This transformation process produces two key outputs when each of your route files are processed:
1. **Reference File**: The bundler plugin takes your original route file (e.g., \`posts.route.tsx\`) and modifies the values for properties like \`component\` or \`pendingComponent\` to use special lazy-loading wrappers that'll fetch the actual code later. These wrappers point to a "virtual" file that the bundler will resolve later on.
2. **Virtual File**: When the bundler sees a request for one of these virtual files (e.g., \`posts.route.tsx?tsr-split=component\`), it intercepts it to generate a new, minimal on-the-fly file that _only_ contains the code for the requested properties (e.g., just the \`PostsComponent\`).
This process ensures that your original code remains clean and readable, while the actual bundled output is optimized for initial bundle size.
### What gets code split?
The decision of what to split into separate chunks is crucial for optimizing your application's performance. TanStack Router uses a concept called "**Split Groupings**" to determine how different parts of your route should be bundled together.
Split groupings are arrays of properties that tell TanStack Router how to bundle different parts of your route together. Each grouping is an list of property names that you want to bundle together into a single lazy-loaded chunk.
The available properties to split are:
- \`component\`
- \`errorComponent\`
- \`pendingComponent\`
- \`notFoundComponent\`
- \`loader\`
By default, TanStack Router uses the following split groupings:
\`\`\`sh
[
['component'],
['errorComponent'],
['notFoundComponent']
]
\`\`\`
This means that it creates three separate lazy-loaded chunks for each route. Resulting in:
- One for the main component
- One for the error component
- And one for the not-found component.
### Rules of Splitting
For automatic code splitting to work, there are some rules in-place to make sure that this process can reliably and predictably happen.
#### Do not export route properties
Route properties like \`component\`, \`loader\`, etc., should not be exported from the route file. Exporting these properties results in them being bundled into the main application bundle, which means that they will not be code-split.
\`\`\`tsx
export const Route = createRoute('/posts')({
// ...
notFoundComponent: PostsNotFoundComponent,
})
// β Do NOT do this!
// Exporting the notFoundComponent will prevent it from being code-split
// and will be included in the main bundle.
export function PostsNotFoundComponent() {
// β
// ...
}
function PostsNotFoundComponent() {
// β
// ...
}
\`\`\`
**That's it!** There are no other restrictions. You can use any other JavaScript or TypeScript features in your route files as you normally would. If you run into any issues, please [open an issue](https://github.com/tanstack/router/issues) on GitHub.
## Granular control
For most applications, the default behavior of using \`autoCodeSplitting: true\` is sufficient. However, TanStack Router provides several options to customize how your routes are split into chunks, allowing you to optimize for specific use cases or performance needs.
### Global code splitting behavior (\`defaultBehavior\`)
You can change how TanStack Router splits your routes by changing the \`defaultBehavior\` option in your bundler plugin configuration. This allows you to define how different properties of your routes should be bundled together.
For example, to bundle all UI-related components into a single chunk, you could configure it like this:
\`\`\`ts title="vite.config.ts"
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
defaultBehavior: [
[
'component',
'pendingComponent',
'errorComponent',
'notFoundComponent',
], // Bundle all UI components together
],
},
}),
],
})
\`\`\`
### Advanced programmatic control (\`splitBehavior\`)
For complex rulesets, you can use the \`splitBehavior\` function in your vite config to programmatically define how routes should be split into chunks based on their \`routeId\`. This function allows you to implement custom logic for grouping properties together, giving you fine-grained control over the code splitting behavior.
\`\`\`ts title="vite.config.ts"
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
splitBehavior: ({ routeId }) => {
// For all routes under /posts, bundle the loader and component together
if (routeId.startsWith('/posts')) {
return [['loader', 'component']]
}
// All other routes will use the \`defaultBehavior\`
},
},
}),
],
})
\`\`\`
### Per-route overrides (\`codeSplitGroupings\`)
For ultimate control, you can override the global configuration directly inside a route file by adding a \`codeSplitGroupings\` property. This is useful for routes that have unique optimization needs.
\`\`\`tsx title="src/routes/posts.route.tsx"
import { loadPostsData } from './-heavy-posts-utils'
export const Route = createFileRoute('/posts')({
// For this specific route, bundle the loader and component together.
codeSplitGroupings: [['loader', 'component']],
loader: () => loadPostsData(),
component: PostsComponent,
})
function PostsComponent() {
// ...
}
\`\`\`
This will create a single chunk that includes both the \`loader\` and the \`component\` for this specific route, overriding both the default behavior and any programmatic split behavior defined in your bundler config.
### Configuration order matters
This guide has so far describe three different ways to configure how TanStack Router splits your routes into chunks.
To make sure that the different configurations do not conflict with each other, TanStack Router uses the following order of precedence:
1. **Per-route overrides**: The \`codeSplitGroupings\` property inside a route file takes the highest precedence. This allows you to define specific split groupings for individual routes.
2. **Programmatic split behavior**: The \`splitBehavior\` function in your bundler config allows you to define custom logic for how routes should be split based on their \`routeId\`.
3. **Default behavior**: The \`defaultBehavior\` option in your bundler config serves as the fallback for any routes that do not have specific overrides or custom logic defined. This is the base configuration that applies to all routes unless overridden.
### Splitting the Data Loader
The \`loader\` function is responsible for fetching data needed by the route. By default, it is bundled with into your "reference file" and loaded in the initial bundle. However, you can also split the \`loader\` into its own chunk if you want to optimize further.
> [!CAUTION]
> Moving the \`loader\` into its own chunk is a **performance trade-off**. It introduces an additional trip to the server before the data can be fetched, which can lead to slower initial page loads. This is because the \`loader\` **must** be fetched and executed before the route can render its component.
> Therefore, we recommend keeping the \`loader\` in the initial bundle unless you have a specific reason to split it.
\`\`\`ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
defaultBehavior: [
['loader'], // The loader will be in its own chunk
['component'],
// ... other component groupings
],
},
}),
],
})
\`\`\`
We highly discourage splitting the \`loader\` unless you have a specific use case that requires it. In most cases, not splitting off the \`loader\` and keep it in the main bundle is the best choice for performance.
# Code Splitting
Code splitting and lazy loading is a powerful technique for improving the bundle size and load performance of an application.
- Reduces the amount of code that needs to be loaded on initial page load
- Code is loaded on-demand when it is needed
- Results in more chunks that are smaller in size that can be cached more easily by the browser.
## How does TanStack Router split code?
TanStack Router separates code into two categories:
- **Critical Route Configuration** - The code that is required to render the current route and kick off the data loading process as early as possible.
- Path Parsing/Serialization
- Search Param Validation
- Loaders, Before Load
- Route Context
- Static Data
- Links
- Scripts
- Styles
- All other route configuration not listed below
- **Non-Critical/Lazy Route Configuration** - The code that is not required to match the route, and can be loaded on-demand.
- Route Component
- Error Component
- Pending Component
- Not-found Component
> π§ **Why is the loader not split?**
>
> - The loader is already an asynchronous boundary, so you pay double to both get the chunk _and_ wait for the loader to execute.
> - Categorically, it is less likely to contribute to a large bundle size than a component.
> - The loader is one of the most important preloadable assets for a route, especially if you're using a default preload intent, like hovering over a link, so it's important for the loader to be available without any additional async overhead.
>
> Knowing the disadvantages of splitting the loader, if you still want to go ahead with it, head over to the [Data Loader Splitting](#data-loader-splitting) section.
## Encapsulating a route's files into a directory
Since TanStack Router's file-based routing system is designed to support both flat and nested file structures, it's possible to encapsulate a route's files into a single directory without any additional configuration.
To encapsulate a route's files into a directory, move the route file itself into a \`.route\` file within a directory with the same name as the route file.
For example, if you have a route file named \`posts.tsx\`, you would create a new directory named \`posts\` and move the \`posts.tsx\` file into that directory, renaming it to \`route.tsx\`.
**Before**
- \`posts.tsx\`
**After**
- \`posts\`
- \`route.tsx\`
## Approaches to code splitting
TanStack Router supports multiple approaches to code splitting. If you are using code-based routing, skip to the [Code-Based Splitting](#code-based-splitting) section.
When you are using file-based routing, you can use the following approaches to code splitting:
- [Using automatic code-splitting β¨](#using-automatic-code-splitting)
- [Using the \`.lazy.tsx\` suffix](#using-the-lazytsx-suffix)
- [Using Virtual Routes](#using-virtual-routes)
## Using automatic code-splittingβ¨
This is the easiest and most powerful way to code split your route files.
When using the \`autoCodeSplitting\` feature, TanStack Router will automatically code split your route files based on the non-critical route configuration mentioned above.
> [!IMPORTANT]
> The automatic code-splitting feature is **ONLY** available when you are using file-based routing with one of our [supported bundlers](../routing/file-based-routing.md#getting-started-with-file-based-routing).
> This will **NOT** work if you are **only** using the CLI (\`@tanstack/router-cli\`).
To enable automatic code-splitting, you just need to add the following to the configuration of your TanStack Router Bundler Plugin:
\`\`\`ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
// ...
autoCodeSplitting: true,
}),
react(), // Make sure to add this plugin after the TanStack Router Bundler plugin
],
})
\`\`\`
That's it! TanStack Router will automatically code-split all your route files by their critical and non-critical route configurations.
If you want more control over the code-splitting process, head over to the [Automatic Code Splitting](./automatic-code-splitting.md) guide to learn more about the options available.
## Using the \`.lazy.tsx\` suffix
If you are not able to use the automatic code-splitting feature, you can still code-split your route files using the \`.lazy.tsx\` suffix. It is **as easy as moving your code into a separate file with a \`.lazy.tsx\` suffix** and using the \`createLazyFileRoute\` function instead of \`createFileRoute\`.
> [!IMPORTANT]
> The \`__root.tsx\` route file, using either \`createRootRoute\` or \`createRootRouteWithContext\`, does not support code splitting, since it's always rendered regardless of the current route.
These are the only options that \`createLazyFileRoute\` supports:
| Export Name | Description |
| ------------------- | --------------------------------------------------------------------- |
| \`component\` | The component to render for the route. |
| \`errorComponent\` | The component to render when an error occurs while loading the route. |
| \`pendingComponent\` | The component to render while the route is loading. |
| \`notFoundComponent\` | The component to render if a not-found error gets thrown. |
### Example code splitting with \`.lazy.tsx\`
When you are using \`.lazy.tsx\` you can split your route into two files to enable code splitting:
**Before (Single File)**
# React
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from './api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
# Solid
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/solid-router'
import { fetchPosts } from './api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
**After (Split into two files)**
This file would contain the critical route configuration:
# React
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from './api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
})
\`\`\`
# Solid
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/solid-router'
import { fetchPosts } from './api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
})
\`\`\`
With the non-critical route configuration going into the file with the \`.lazy.tsx\` suffix:
# React
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
# Solid
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/solid-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
## Using Virtual Routes
You might run into a situation where you end up splitting out everything from a route file, leaving it empty! In this case, simply **delete the route file entirely**! A virtual route will automatically be generated for you to serve as an anchor for your code split files. This virtual route will live directly in the generated route tree file.
**Before (Virtual Routes)**
# React
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
// Hello?
})
\`\`\`
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
# Solid
\`\`\`tsx title="src/routes/posts.tsx"
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/posts')({
// Hello?
})
\`\`\`
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/solid-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
**After (Virtual Routes)**
# React
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
# Solid
\`\`\`tsx title="src/routes/posts.lazy.tsx"
import { createLazyFileRoute } from '@tanstack/solid-router'
export const Route = createLazyFileRoute('/posts')({
component: Posts,
})
function Posts() {
// ...
}
\`\`\`
Tada! π
## Code-Based Splitting
If you are using code-based routing, you can still code-split your routes using the \`Route.lazy()\` method and the \`createLazyRoute\` function. You'll need to split your route configuration into two parts:
Create a lazy route using the \`createLazyRoute\` function.
\`\`\`tsx title="src/posts.lazy.tsx"
export const Route = createLazyRoute('/posts')({
component: MyComponent,
})
function MyComponent() {
return
My Component
}
\`\`\`
Then, call the \`.lazy\` method on the route definition in your \`app.tsx\` to import the lazy/code-split route with the non-critical route configuration.
\`\`\`tsx title="src/app.tsx"
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
\`\`\`
## Data Loader Splitting
**Be warned!!!** Splitting a route loader is a dangerous game.
It can be a powerful tool to reduce bundle size, but it comes with a cost as mentioned in the [How does TanStack Router split code?](#how-does-tanstack-router-split-code) section.
You can code split your data loading logic using the Route's \`loader\` option. While this process makes it difficult to maintain type-safety with the parameters passed to your loader, you can always use the generic \`LoaderContext\` type to get you most of the way there:
# React
\`\`\`tsx
import { lazyFn } from '@tanstack/react-router'
const route = createRoute({
path: '/my-route',
component: MyComponent,
loader: lazyFn(() => import('./loader'), 'loader'),
})
// In another file...a
export const loader = async (context: LoaderContext) => {
/// ...
}
\`\`\`
# Solid
\`\`\`tsx
import { lazyFn } from '@tanstack/solid-router'
const route = createRoute({
path: '/my-route',
component: MyComponent,
loader: lazyFn(() => import('./loader'), 'loader'),
})
// In another file...a
export const loader = async (context: LoaderContext) => {
/// ...
}
\`\`\`
If you are using file-based routing, you'll only be able to split your \`loader\` if you are using [Automatic Code Splitting](#using-automatic-code-splitting) with customized bundling options.
## Manually accessing Route APIs in other files with the \`getRouteApi\` helper
As you might have guessed, placing your component code in a separate file than your route can make it difficult to consume the route itself. To help with this, TanStack Router exports a handy \`getRouteApi\` function that you can use to access a route's type-safe APIs in a file without importing the route itself.
# React
\`\`\`tsx title="src/my-route.tsx"
import { createRoute } from '@tanstack/react-router'
import { MyComponent } from './MyComponent'
const route = createRoute({
path: '/my-route',
loader: () => ({
foo: 'bar',
}),
component: MyComponent,
})
\`\`\`
\`\`\`tsx title="src/MyComponent.tsx"
import { getRouteApi } from '@tanstack/react-router'
const route = getRouteApi('/my-route')
export function MyComponent() {
const loaderData = route.useLoaderData()
// ^? { foo: string }
return
}
\`\`\`
The \`getRouteApi\` function is useful for accessing other type-safe APIs:
- \`useLoaderData\`
- \`useLoaderDeps\`
- \`useMatch\`
- \`useParams\`
- \`useRouteContext\`
- \`useSearch\`
# Creating a Router
## The \`createRouter\` function
When you're ready to start using your router, you'll need to create a new \`Router\` instance. The router instance is the core brains of TanStack Router and is responsible for managing the route tree, matching routes, and coordinating navigations and route transitions. It also serves as a place to configure router-wide settings.
# React
\`\`\`tsx title="src/router.tsx"
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
// ...
})
\`\`\`
# Solid
\`\`\`tsx title="src/router.tsx"
import { createRouter } from '@tanstack/solid-router'
const router = createRouter({
// ...
})
\`\`\`
## Route Tree
You'll probably notice quickly that the \`Router\` constructor requires a \`routeTree\` option. This is the route tree that the router will use to match routes and render components.
Whether you used [file-based routing](../routing/file-based-routing.md) or [code-based routing](../routing/code-based-routing.md), you'll need to pass your route tree to the \`createRouter\` function:
### Filesystem Route Tree
If you used our recommended file-based routing, then it's likely your generated route tree file was created at the default \`src/routeTree.gen.ts\` location. If you used a custom location, then you'll need to import your route tree from that location.
\`\`\`tsx
import { routeTree } from './routeTree.gen'
\`\`\`
### Code-Based Route Tree
If you used code-based routing, then you likely created your route tree manually using the root route's \`addChildren\` method:
\`\`\`tsx
const routeTree = rootRoute.addChildren([
// ...
])
\`\`\`
## Router Type Safety
> [!IMPORTANT]
> DO NOT SKIP THIS SECTION! β οΈ
TanStack Router provides amazing support for TypeScript, even for things you wouldn't expect like bare imports straight from the library! To make this possible, you must register your router's types using TypeScripts' [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) feature. This is done by extending the \`Register\` interface on \`@tanstack/react-router\` with a \`router\` property that has the type of your \`router\` instance:
# React
\`\`\`tsx title="src/router.tsx"
declare module '@tanstack/react-router' {
interface Register {
// This infers the type of our router and registers it across your entire project
router: typeof router
}
}
\`\`\`
# Solid
\`\`\`tsx title="src/router.tsx"
declare module '@tanstack/solid-router' {
interface Register {
// This infers the type of our router and registers it across your entire project
router: typeof router
}
}
\`\`\`
With your router registered, you'll now get type-safety across your entire project for anything related to routing.
## 404 Not Found Route
As promised in earlier guides, we'll now cover the \`notFoundRoute\` option. This option is used to configure a route that will render when no other suitable match is found. This is useful for rendering a 404 page or redirecting to a default route.
If you are using either file-based or code-based routing, then you'll need to add a \`notFoundComponent\` key to \`createRootRoute\`:
\`\`\`tsx
export const Route = createRootRoute({
component: () => (
// ...
),
notFoundComponent: () =>
404 Not Found
,
});
\`\`\`
## Other Options
There are many other options that can be passed to the \`Router\` constructor. You can find a full list of them in the [API Reference](../api/router/RouterOptionsType.md).
# Custom Link
While repeating yourself can be acceptable in many situations, you might find that you do it too often. At times, you may want to create cross-cutting components with additional behavior or styles. You might also consider using third-party libraries in combination with TanStack Router's type safety.
## \`createLink\` for cross-cutting concerns
\`createLink\` creates a custom \`Link\` component with the same type parameters as \`Link\`. This means you can create your own component which provides the same type safety and typescript performance as \`Link\`.
### Basic example
If you want to create a basic custom link component, you can do so with the following:
# React
\`\`\`tsx
import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
interface BasicLinkProps extends React.AnchorHTMLAttributes {
// Add any additional props you want to pass to the anchor element
}
const BasicLinkComponent = React.forwardRef(
(props, ref) => {
return (
)
},
)
const CreatedLinkComponent = createLink(BasicLinkComponent)
export const CustomLink: LinkComponent = (props) => {
return
}
\`\`\`
# Solid
\`\`\`tsx
import * as Solid from 'solid-js'
import { createLink, LinkComponent } from '@tanstack/solid-router'
export const Route = createRootRoute({
component: RootComponent,
})
type BasicLinkProps = Solid.JSX.IntrinsicElements['a'] & {
// Add any additional props you want to pass to the anchor element
}
const BasicLinkComponent: Solid.Component = (props) => (
{props.children}
)
const CreatedLinkComponent = createLink(BasicLinkComponent)
export const CustomLink: LinkComponent = (props) => {
return
}
\`\`\`
You can then use your newly created \`Link\` component as any other \`Link\`
\`\`\`tsx
\`\`\`
## \`createLink\` with third party libraries
Here are some examples of how you can use \`createLink\` with third-party libraries.
# React
### React Aria Components example
React Aria Components v1.11.0 and later works with TanStack Router's \`preload (intent)\` prop. Use \`createLink\` to wrap each React Aria component that you use as a link.
\`\`\`tsx title="RACLink.tsx"
import { createLink } from '@tanstack/react-router'
import { Link as RACLink, MenuItem } from 'react-aria-components'
export const Link = createLink(RACLink)
export const MenuItemLink = createLink(MenuItem)
\`\`\`
\`\`\`tsx title="CustomRACLink.tsx"
import { createLink } from '@tanstack/react-router'
import { Link as RACLink, type LinkProps } from 'react-aria-components'
interface MyLinkProps extends LinkProps {
// your props
}
function MyLink(props: MyLinkProps) {
return (
({
color: isHovered ? 'red' : 'blue',
})}
/>
)
}
export const Link = createLink(MyLink)
\`\`\`
To use React Aria's render props, including the \`className\`, \`style\`, and \`children\` functions, create a wrapper component and pass that to \`createLink\`.
# React
### Chakra UI example
\`\`\`tsx title="ChakraLinkComponent.tsx"
import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
import { Link } from '@chakra-ui/react'
interface ChakraLinkProps extends Omit<
React.ComponentPropsWithoutRef,
'href'
> {
// Add any additional props you want to pass to the link
}
const ChakraLinkComponent = React.forwardRef<
HTMLAnchorElement,
ChakraLinkProps
>((props, ref) => {
return
})
const CreatedLinkComponent = createLink(ChakraLinkComponent)
export const CustomLink: LinkComponent = (
props,
) => {
return (
)
}
\`\`\`
# React
### MUI example
There is an [example](https://github.com/TanStack/router/tree/main/examples/react/start-material-ui) available which uses these patterns.
#### \`Link\`
If the MUI \`Link\` should simply behave like the router \`Link\`, it can be just wrapped with \`createLink\`:
\`\`\`tsx title="CustomLink.tsx"
import { createLink } from '@tanstack/react-router'
import { Link } from '@mui/material'
export const CustomLink = createLink(Link)
\`\`\`
If the \`Link\` should be customized this approach can be used:
\`\`\`tsx title="CustomLink.tsx"
import React from 'react'
import { createLink } from '@tanstack/react-router'
import { Link } from '@mui/material'
import type { LinkProps } from '@mui/material'
import type { LinkComponent } from '@tanstack/react-router'
interface MUILinkProps extends LinkProps {
// Add any additional props you want to pass to the Link
}
const MUILinkComponent = React.forwardRef(
(props, ref) => ,
)
const CreatedLinkComponent = createLink(MUILinkComponent)
export const CustomLink: LinkComponent = (props) => {
return
}
// Can also be styled
\`\`\`
#### \`Button\`
If a \`Button\` should be used as a router \`Link\`, the \`component\` should be set as \`a\`:
\`\`\`tsx title="CustomButtonLink.tsx"
import React from 'react'
import { createLink } from '@tanstack/react-router'
import { Button } from '@mui/material'
import type { ButtonProps } from '@mui/material'
import type { LinkComponent } from '@tanstack/react-router'
interface MUIButtonLinkProps extends ButtonProps<'a'> {
// Add any additional props you want to pass to the Button
}
const MUIButtonLinkComponent = React.forwardRef<
HTMLAnchorElement,
MUIButtonLinkProps
>((props, ref) => )
const CreatedButtonLinkComponent = createLink(MUIButtonLinkComponent)
export const CustomButtonLink: LinkComponent = (
props,
) => {
return
}
\`\`\`
#### Usage with \`styled\`
Any of these MUI approaches can then be used with \`styled\`:
\`\`\`tsx title="StyledCustomLink.tsx"
import { css, styled } from '@mui/material'
import { CustomLink } from './CustomLink'
const StyledCustomLink = styled(CustomLink)(
({ theme }) => css\`
color: \${theme.palette.common.white};
\`,
)
\`\`\`
# React
### Mantine example
\`\`\`tsx title="CustomLink.tsx"
import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
import { Anchor, AnchorProps } from '@mantine/core'
interface MantineAnchorProps extends Omit {
// Add any additional props you want to pass to the anchor
}
const MantineLinkComponent = React.forwardRef<
HTMLAnchorElement,
MantineAnchorProps
>((props, ref) => {
return
})
const CreatedLinkComponent = createLink(MantineLinkComponent)
export const CustomLink: LinkComponent = (
props,
) => {
return
}
\`\`\`
# Solid
### Some Library example
\`\`\`tsx title="UntitledLink.tsx"
// TODO: Add this example.
\`\`\`
# Custom Search Param Serialization
By default, TanStack Router parses and serializes your URL Search Params automatically using \`JSON.stringify\` and \`JSON.parse\`. This process involves escaping and unescaping the search string, which is a common practice for URL search params, in addition to the serialization and deserialization of the search object.
For instance, using the default configuration, if you have the following search object:
\`\`\`tsx
const search = {
page: 1,
sort: 'asc',
filters: { author: 'tanner', min_words: 800 },
}
\`\`\`
It would be serialized and escaped into the following search string:
\`\`\`txt
?page=1&sort=asc&filters=%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D
\`\`\`
We can implement the default behavior with the following code:
# React
\`\`\`tsx
import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
})
\`\`\`
# Solid
\`\`\`tsx
import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
})
\`\`\`
However, this default behavior may not be suitable for all use cases. For example, you may want to use a different serialization format, such as base64 encoding, or you may want to use a purpose-built serialization/deserialization library, like [query-string](https://github.com/sindresorhus/query-string), [JSURL2](https://github.com/wmertens/jsurl2), or [Zipson](https://jgranstrom.github.io/zipson/).
This can be achieved by providing your own serialization and deserialization functions to the \`parseSearch\` and \`stringifySearch\` options in the [\`Router\`](../api/router/RouterOptionsType.md#stringifysearch-method) configuration. When doing this, you can utilize TanStack Router's built-in helper functions, \`parseSearchWith\` and \`stringifySearchWith\`, to simplify the process.
> [!TIP]
> An important aspect of serialization and deserialization, is that you are able to get the same object back after deserialization. This is important because if the serialization and deserialization process is not done correctly, you may lose some information. For example, if you are using a library that does not support nested objects, you may lose the nested object when deserializing the search string.

Here are some examples of how you can customize the search param serialization in TanStack Router:
## Using Base64
It's common to base64 encode your search params to achieve maximum compatibility across browsers and URL unfurlers, etc. This can be done with the following code:
# React
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
const router = createRouter({
parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(JSON.stringify(value)),
),
})
function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}
function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}
\`\`\`
# Solid
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
const router = createRouter({
parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(JSON.stringify(value)),
),
})
function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}
function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}
\`\`\`
> [β οΈ Why does this snippet not use atob/btoa?](#safe-binary-encodingdecoding)
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
\`\`\`txt
?page=1&sort=asc&filters=eyJhdXRob3IiOiJ0YW5uZXIiLCJtaW5fd29yZHMiOjgwMH0%3D
\`\`\`
> [!WARNING]
> If you are serializing user input into Base64, you run the risk of causing a collision with the URL deserialization. This can lead to unexpected behavior, such as the URL not being parsed correctly or being interpreted as a different value. To avoid this, you should encode the search params using a safe binary encoding/decoding method (see below).
## Using the query-string library
The [query-string](https://github.com/sindresorhus/query-string) library is a popular for being able to reliably parse and stringify query strings. You can use it to customize the serialization format of your search params. This can be done with the following code:
# React
\`\`\`tsx
import { createRouter } from '@tanstack/react-router'
import qs from 'query-string'
const router = createRouter({
// ...
stringifySearch: stringifySearchWith((value) =>
qs.stringify(value, {
// ...options
}),
),
parseSearch: parseSearchWith((value) =>
qs.parse(value, {
// ...options
}),
),
})
\`\`\`
# Solid
\`\`\`tsx
import { createRouter } from '@tanstack/solid-router'
import qs from 'query-string'
const router = createRouter({
// ...
stringifySearch: stringifySearchWith((value) =>
qs.stringify(value, {
// ...options
}),
),
parseSearch: parseSearchWith((value) =>
qs.parse(value, {
// ...options
}),
),
})
\`\`\`
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
\`\`\`txt
?page=1&sort=asc&filters=author%3Dtanner%26min_words%3D800
\`\`\`
## Using the JSURL2 library
[JSURL2](https://github.com/wmertens/jsurl2) is a non-standard library that can compress URLs while still maintaining readability. This can be done with the following code:
# React
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
import { parse, stringify } from 'jsurl2'
const router = createRouter({
// ...
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})
\`\`\`
# Solid
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
import { parse, stringify } from 'jsurl2'
const router = createRouter({
// ...
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})
\`\`\`
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
\`\`\`txt
?page=1&sort=asc&filters=(author~tanner~min*_words~800)~
\`\`\`
## Using the Zipson library
[Zipson](https://jgranstrom.github.io/zipson/) is a very user-friendly and performant JSON compression library (both in runtime performance and the resulting compression performance). To compress your search params with it (which requires escaping/unescaping and base64 encoding/decoding them as well), you can use the following code:
# React
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
import { stringify, parse } from 'zipson'
const router = createRouter({
parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(stringify(value)),
),
})
function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}
function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}
\`\`\`
# Solid
\`\`\`tsx
import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
import { stringify, parse } from 'zipson'
const router = createRouter({
parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(stringify(value)),
),
})
function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}
function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}
\`\`\`
> [β οΈ Why does this snippet not use atob/btoa?](#safe-binary-encodingdecoding)
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
\`\`\`txt
?page=1&sort=asc&filters=JTdCJUMyJUE4YXV0aG9yJUMyJUE4JUMyJUE4dGFubmVyJUMyJUE4JUMyJUE4bWluX3dvcmRzJUMyJUE4JUMyJUEyQ3UlN0Q%3D
\`\`\`
## Safe Binary Encoding/Decoding
In the browser, the \`atob\` and \`btoa\` functions are not guaranteed to work properly with non-UTF8 characters. We recommend using these encoding/decoding utilities instead:
To encode from a string to a binary string:
\`\`\`ts
export function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}
\`\`\`
To decode from a binary string to a string:
\`\`\`ts
export function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}
\`\`\`
# Data Loading
Data loading is a common concern for web applications and is related to routing. When loading a page for your app, it's ideal if all of the page's async requirements are fetched and fulfilled as early as possible, in parallel. The router is the best place to coordinate these async dependencies as it's usually the only place in your app that knows where users are headed before content is rendered.
You may be familiar with \`getServerSideProps\` from Next.js or \`loader\`s from Remix/React-Router. TanStack Router has similar functionality to preload/load assets on a per-route basis in parallel allowing it to render as quickly as possible as it fetches via suspense.
Beyond these normal expectations of a router, TanStack Router goes above and beyond and provides **built-in SWR Caching**, a long-term in-memory caching layer for route loaders. This means that you can use TanStack Router to both preload data for your routes so they load instantaneously or temporarily cache route data for previously visited routes to use again later.
## The route loading lifecycle
Every time a URL/history update is detected, the router executes the following sequence:
- Route Matching (Top-Down)
- \`route.params.parse\`
- \`route.validateSearch\`
- Route Pre-Loading (Serial)
- \`route.beforeLoad\`
- \`route.onError\`
- \`route.errorComponent\` / \`parentRoute.errorComponent\` / \`router.defaultErrorComponent\`
- Route Loading (Parallel)
- \`route.component.preload?\`
- \`route.loader\`
- \`route.pendingComponent\` (Optional)
- \`route.component\`
- \`route.onError\`
- \`route.errorComponent\` / \`parentRoute.errorComponent\` / \`router.defaultErrorComponent\`
## To Router Cache or not to Router Cache?
There is a high possibility that TanStack's router cache will be a good fit for most smaller to medium size applications, but it's important to understand the tradeoffs of using it vs a more robust caching solution like TanStack Query:
TanStack Router Cache Pros:
- Built-in, easy to use, no extra dependencies
- Handles deduping, preloading, loading, stale-while-revalidate, background refetching on a per-route basis
- Coarse invalidation (invalidate all routes and cache at once)
- Automatic garbage collection
- Works great for apps that share little data between routes
- "Just works" for SSR
TanStack Router Cache Cons:
- No persistence adapters/model
- No shared caching/deduping between routes
- No built-in mutation APIs (a basic \`useMutation\` hook is provided in many examples that may be sufficient for many use cases)
- No built-in cache-level optimistic update APIs (you can still use ephemeral state from something like a \`useMutation\` hook to achieve this at the component level)
> [!TIP]
> If you know right away that you'd like to or need to use something more robust like TanStack Query, skip to the [External Data Loading](./external-data-loading.md) guide.
## Using the Router Cache
The router cache is built-in and is as easy as returning data from any route's \`loader\` function. Let's learn how!
## Route \`loader\`s
Route \`loader\` functions are called when a route match is loaded. They are called with a single parameter which is an object containing many helpful properties. We'll go over those in a bit, but first, let's look at the two supported \`loader\` forms:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
\`\`\`
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: {
handler: () => fetchPosts(),
},
})
\`\`\`
Use the object form when you want to configure loader-specific behavior such as \`staleReloadMode\`.
## \`loader\` Parameters
The \`loader\` function receives a single object with the following properties:
- \`abortController\` - The route's abortController. Its signal is cancelled when the route is unloaded or when the Route is no longer relevant and the current invocation of the \`loader\` function becomes outdated.
- \`cause\` - The cause of the current route match. Can be either one of the following:
- \`enter\` - When the route is matched and loaded after not being matched in the previous location.
- \`preload\` - When the route is being preloaded.
- \`stay\` - When the route is matched and loaded after being matched in the previous location.
- \`context\` - The route's context object, which is a merged union of:
- Parent route context
- This route's context as provided by the \`beforeLoad\` option
- \`deps\` - The object value returned from the \`Route.loaderDeps\` function. If \`Route.loaderDeps\` is not defined, an empty object will be provided instead.
- \`location\` - The current location
- \`params\` - The route's path params
- \`parentMatchPromise\` - \`Promise\` (\`undefined\` for the root route)
- \`preload\` - Boolean which is \`true\` when the route is being preloaded instead of loaded
- \`route\` - The route itself
Using these parameters, we can do a lot of cool things, but first, let's take a look at how we can control it and when the \`loader\` function is called.
## Consuming data from \`loader\`s
To consume data from a \`loader\`, use the \`useLoaderData\` hook defined on your Route object.
\`\`\`tsx
const posts = Route.useLoaderData()
\`\`\`
If you don't have ready access to your route object (i.e. you're deep in the component tree for the current route), you can use \`getRouteApi\` to access the same hook (as well as the other hooks on the Route object). This should be preferred over importing the Route object, which is likely to create circular dependencies.
# React
\`\`\`tsx
import { getRouteApi } from '@tanstack/react-router'
// in your component
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
\`\`\`
# Solid
\`\`\`tsx
import { getRouteApi } from '@tanstack/solid-router'
// in your component
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
\`\`\`
## Dependency-based Stale-While-Revalidate Caching
TanStack Router provides a built-in Stale-While-Revalidate caching layer for route loaders that is keyed on the dependencies of a route:
- The route's fully parsed pathname
- e.g. \`/posts/1\` vs \`/posts/2\`
- Any additional dependencies provided by the \`loaderDeps\` option
- e.g. \`loaderDeps: ({ search: { pageIndex, pageSize } }) => ({ pageIndex, pageSize })\`
Using these dependencies as keys, TanStack Router will cache the data returned from a route's \`loader\` function and use it to fulfill subsequent requests for the same route match. This means that if a route's data is already in the cache, it will be returned immediately, then **potentially** be refetched in the background depending on the "freshness" of the data.
### Key options
To control router dependencies and "freshness", TanStack Router provides a plethora of options to control the keying and caching behavior of your route loaders. Let's take a look at them in the order that you are most likely to use them:
- \`routeOptions.loaderDeps\`
- A function that supplies you the search params for a router and returns an object of dependencies for use in your \`loader\` function. When these deps changed from navigation to navigation, it will cause the route to reload regardless of \`staleTime\`s. The deps are compared using a deep equality check.
- \`routeOptions.staleTime\`
- \`routerOptions.defaultStaleTime\`
- The number of milliseconds that a route's data should be considered fresh when attempting to load.
- \`routeOptions.preloadStaleTime\`
- \`routerOptions.defaultPreloadStaleTime\`
- The number of milliseconds that a route's data should be considered fresh attempting to preload.
- \`routeOptions.gcTime\`
- \`routerOptions.defaultGcTime\`
- The number of milliseconds that a route's data should be kept in the cache before being garbage collected.
- \`routeOptions.shouldReload\`
- A function that receives the same \`beforeLoad\` and \`loaderContext\` parameters and returns a boolean indicating if the route should reload. This offers one more level of control over when a route should reload beyond \`staleTime\` and \`loaderDeps\` and can be used to implement patterns similar to Remix's \`shouldLoad\` option.
- \`routeOptions.loader.staleReloadMode\`
- \`routerOptions.defaultStaleReloadMode\`
- Controls what happens when a matched route already has stale successful data. Use \`'background'\` for stale-while-revalidate, or \`'blocking'\` to wait for the stale loader reload to finish before continuing.
### β οΈ Some Important Defaults
- By default, the \`staleTime\` is set to \`0\`, meaning that the route's data is immediately considered stale. Stale matches are reloaded in the background when the route is entered again, when its loader key changes (path params used by the route or \`loaderDeps\`), or when \`router.load()\` is called explicitly.
- By default, a previously preloaded route is considered fresh for **30 seconds**. This means if a route is preloaded, then preloaded again within 30 seconds, the second preload will be ignored. This prevents unnecessary preloads from happening too frequently. **When a route is loaded normally, the standard \`staleTime\` is used.**
- By default, the \`gcTime\` is set to **30 minutes**, meaning that any route data that has not been accessed in 30 minutes will be garbage collected and removed from the cache.
- By default, \`staleReloadMode\` is \`'background'\`, so stale successful matches keep rendering with their existing \`loaderData\` while the loader revalidates in the background.
- \`router.invalidate()\` will force all active routes to reload their loaders immediately and mark every cached route's data as stale.
### Using \`loaderDeps\` to access search params
Imagine a \`/posts\` route supports some pagination via search params \`offset\` and \`limit\`. For the cache to uniquely store this data, we need to access these search params via the \`loaderDeps\` function. By explicitly identifying them, each route match for \`/posts\` with different \`offset\` and \`limit\` won't get mixed up!
Once we have these deps in place, the route will always reload when the deps change.
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) =>
fetchPosts({
offset,
limit,
}),
})
\`\`\`
> [!WARNING]
> **Only include dependencies you actually use in the loader.**
>
> A common mistake is returning the entire \`search\` object:
>
> \`\`\`tsx
> // β Don't do this - causes unnecessary cache invalidation
> loaderDeps: ({ search }) => search,
> loader: ({ deps }) => fetchPosts({ page: deps.page }), // only uses page!
> \`\`\`
>
> This causes the route to reload whenever ANY search param changes, even params not used in the loader (like \`viewMode\` or \`sortDirection\`). Instead, extract only what you need:
>
> \`\`\`tsx
> // β Do this - only reload when used params change
> loaderDeps: ({ search }) => ({
> page: search.page,
> limit: search.limit,
> }),
> loader: ({ deps }) => fetchPosts(deps),
> \`\`\`
### Using \`staleTime\` to control how long data is considered fresh
By default, \`staleTime\` for navigations is set to \`0\`ms (and 30 seconds for preloads) which means that the route's data will always be considered stale. When a stale route is entered again, its loader key changes, or \`router.load()\` is called explicitly, the route will reload in the background.
**This is a good default for most use cases, but you may find that some route data is more static or potentially expensive to load.** In these cases, you can use the \`staleTime\` option to control how long the route's data is considered fresh for navigations. Let's take a look at an example:
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// Consider the route's data fresh for 10 seconds
staleTime: 10_000,
})
\`\`\`
By passing \`10_000\` to the \`staleTime\` option, we are telling the router to consider the route's data fresh for 10 seconds. This means that if the user navigates to \`/posts\` from \`/about\` within 10 seconds of the last loader result, the route's data will not be reloaded. If the user then navigates to \`/posts\` from \`/about\` after 10 seconds, the route's data will be reloaded **in the background**.
## Choosing background vs blocking stale reloads
By default, stale successful matches use stale-while-revalidate behavior. That means the router can render with the existing \`loaderData\` immediately and then refresh it in the background.
If you want a specific loader to wait for a stale reload to finish before continuing, use the object form and set \`staleReloadMode: 'blocking'\`:
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: {
handler: () => fetchPosts(),
staleReloadMode: 'blocking',
},
})
\`\`\`
You can also change the default for the entire router:
\`\`\`tsx
const router = createRouter({
routeTree,
defaultStaleReloadMode: 'blocking',
})
\`\`\`
Use \`'background'\` when showing stale data during revalidation is acceptable. Use \`'blocking'\` when you want stale matches to behave more like a fresh load and wait for the new loader result.
## Turning off automatic stale reloads
To disable automatic stale reloads for a route, set the \`staleTime\` option to \`Infinity\`:
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
\`\`\`
You can even turn this off for all routes by setting the \`defaultStaleTime\` option on the router:
\`\`\`tsx
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
\`\`\`
This differs from \`staleReloadMode: 'blocking'\`:
- \`staleTime: Infinity\` prevents the route from becoming stale in the first place
- \`staleReloadMode: 'blocking'\` still allows stale reloads, but waits for them instead of doing them in the background
## Using \`shouldReload\` and \`gcTime\` to opt-out of caching
Similar to Remix's default functionality, you may want to configure a route to only load on entry or when critical loader deps change. You can do this by using the \`gcTime\` option combined with the \`shouldReload\` option, which accepts either a \`boolean\` or a function that receives the same \`beforeLoad\` and \`loaderContext\` parameters and returns a boolean indicating if the route should reload.
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// Do not cache this route's data after it's unloaded
gcTime: 0,
// Only reload the route when the user navigates to it or when deps change
shouldReload: false,
})
\`\`\`
### Opting out of caching while still preloading
Even though you may opt-out of short-term caching for your route data, you can still get the benefits of preloading! With the above configuration, preloading will still "just work" with the default \`preloadGcTime\`. This means that if a route is preloaded, then navigated to, the route's data will be considered fresh and will not be reloaded.
To opt out of preloading, don't turn it on via the \`routerOptions.defaultPreload\` or \`routeOptions.preload\` options.
## Passing all loader events to an external cache
We break down this use case in the [External Data Loading](./external-data-loading.md) page, but if you'd like to use an external cache like TanStack Query, you can do so by passing all loader events to your external cache. As long as you are using the defaults, the only change you'll need to make is to set the \`defaultPreloadStaleTime\` option on the router to \`0\`:
\`\`\`tsx
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
\`\`\`
This will ensure that every preload, load, and reload event will trigger your \`loader\` functions, which can then be handled and deduped by your external cache.
## Using Router Context
The \`context\` argument passed to the \`loader\` function is an object containing a merged union of:
- Parent route context
- This route's context as provided by the \`beforeLoad\` option
Starting at the very top of the router, you can pass an initial context to the router via the \`context\` option. This context will be available to all routes in the router and get copied and extended by each route as they are matched. This happens by passing a context to a route via the \`beforeLoad\` option. This context will be available to all the route's child routes. The resulting context will be available to the route's \`loader\` function.
In this example, we'll create a function in our route context to fetch posts, then use it in our \`loader\` function.
> π§ Context is a powerful tool for dependency injection. You can use it to inject services, hooks, 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 \`beforeLoad\` option.
- \`/utils/fetchPosts.tsx\`
\`\`\`tsx
export const fetchPosts = async () => {
const res = await fetch(\`/api/posts?page=\${pageIndex}\`)
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
\`\`\`
- \`/routes/__root.tsx\`
# React
\`\`\`tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
\`\`\`
# Solid
\`\`\`tsx
import { createRootRouteWithContext } from '@tanstack/solid-router'
// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
\`\`\`
- \`/routes/posts.tsx\`
\`\`\`tsx
// Notice how our postsRoute references context to get our fetchPosts function
// This can be a powerful tool for dependency injection across your router
// and routes.
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
\`\`\`
- \`/router.tsx\`
\`\`\`tsx
import { routeTree } from './routeTree.gen'
// Use your routerContext to create a new router
// This will require that you fullfil the type requirements of the routerContext
const router = createRouter({
routeTree,
context: {
// Supply the fetchPosts function to the router context
fetchPosts,
},
})
\`\`\`
## Using Path Params
To use path params in your \`loader\` function, access them via the \`params\` property on the function's parameters. Here's an example:
\`\`\`tsx
// src/routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId } }) => fetchPostById(postId),
})
\`\`\`
## Using Route Context
Passing down global context to your router is great, but what if you want to provide context that is specific to a route? This is where the \`beforeLoad\` option comes in. The \`beforeLoad\` option is a function that runs right before attempting to load a route and receives the same parameters as \`loader\`. Beyond its ability to redirect potential matches, block loader requests, etc, it can also return an object that will be merged into the route's context. Let's take a look at an example where we inject some data into our route context via the \`beforeLoad\` option:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
// Pass the fetchPosts function to the route context
beforeLoad: () => ({
fetchPosts: () => console.info('foo'),
}),
loader: ({ context: { fetchPosts } }) => {
fetchPosts() // 'foo'
// ...
},
})
\`\`\`
## Using Search Params in Loaders
> β But wait Tanner... where the heck are my search params?!
You might be here wondering why \`search\` isn't directly available in the \`loader\` function's parameters. We've purposefully designed it this way to help you succeed. Let's take a look at why:
- Search Parameters being used in a loader function are a very good indicator that those search params should also be used to uniquely identify the data being loaded. For example, you may have a route that uses a search param like \`pageIndex\` that uniquely identifies the data held inside of the route match. Or, imagine a \`/users/user\` route that uses the search param \`userId\` to identify a specific user in your application, you might model your url like this: \`/users/user?userId=123\`. This means that your \`user\` route would need some extra help to identify a specific user.
- Directly accessing search params in a loader function can lead to bugs in caching and preloading where the data being loaded is not unique to the current URL pathname and search params. For example, you might ask your \`/posts\` route to preload page 2's results, but without the distinction of pages in your route configuration, you will end up fetching, storing and displaying page 2's data on your \`/posts\` or \`?page=1\` screen instead of it preloading in the background!
- Placing a threshold between search parameters and the loader function allows the router to understand your dependencies and reactivity.
\`\`\`tsx
// /routes/users.user.tsx
export const Route = createFileRoute('/users/user')({
validateSearch: (search) =>
search as {
userId: string
},
loaderDeps: ({ search: { userId } }) => ({
userId,
}),
loader: async ({ deps: { userId } }) => getUser(userId),
})
\`\`\`
### Accessing Search Params via \`routeOptions.loaderDeps\`
\`\`\`tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
// Use zod to validate and parse the search params
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// Pass the offset to your loader deps via the loaderDeps function
loaderDeps: ({ search: { offset } }) => ({ offset }),
// Use the offset from context in the loader function
loader: async ({ deps: { offset } }) =>
fetchPosts({
offset,
}),
})
\`\`\`
## Using the Abort Signal
The \`abortController\` property of the \`loader\` function is an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Its signal 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 it with a fetch call:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ abortController }) =>
fetchPosts({
// Pass this to an underlying fetch call or anything that supports signals
signal: abortController.signal,
}),
})
\`\`\`
## Using the \`preload\` flag
The \`preload\` property of the \`loader\` function is a boolean which is \`true\` when the route is being preloaded instead of loaded. Some data loading libraries may handle preloading differently than a standard fetch, so you may want to pass \`preload\` to your data loading library, or use it to execute the appropriate data loading logic:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ preload }) =>
fetchPosts({
maxAge: preload ? 10_000 : 0, // Preloads should hang around a bit longer
}),
})
\`\`\`
## Handling Slow Loaders
Ideally most route loaders can resolve their data within a short moment, removing the need to render a placeholder spinner and simply rely on suspense to render the next route when it's completely ready. When critical data that is required to render a route's component is slow though, you have 2 options:
- Split up your fast and slow data into separate promises and \`defer\` the slow data until after the fast data is loaded (see the [Deferred Data Loading](./deferred-data-loading.md) guide).
- Show a pending component after an optimistic suspense threshold until all of the data is ready (See below).
## Showing a pending component
**By default, TanStack Router will show a pending component for loaders that take longer than 1 second to resolve.** This is an optimistic threshold that can be configured via:
- \`routeOptions.pendingMs\` or
- \`routerOptions.defaultPendingMs\`
When the pending time threshold is exceeded, the router will render the \`pendingComponent\` option of the route, if configured.
## Avoiding Pending Component Flash
If you're using a pending component, the last thing you want is for your pending time threshold to be met, then have your data resolve immediately after, resulting in a jarring flash of your pending component. To avoid this, **TanStack Router by default will show your pending component for at least 500ms**. This is an optimistic threshold that can be configured via:
- \`routeOptions.pendingMinMs\` or
- \`routerOptions.defaultPendingMinMs\`
## Handling Errors
TanStack Router provides a few ways to handle errors that occur during the route loading lifecycle. Let's take a look at them.
### Handling Errors with \`routeOptions.onError\`
The \`routeOptions.onError\` option is a function that is called when an error occurs during the route loading.
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
onError: ({ error }) => {
// Log the error
console.error(error)
},
})
\`\`\`
### Handling Errors with \`routeOptions.onCatch\`
The \`routeOptions.onCatch\` option is a function that is called whenever an error was caught by the router's CatchBoundary.
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
onCatch: ({ error, errorInfo }) => {
// Log the error
console.error(error)
},
})
\`\`\`
### Handling Errors with \`routeOptions.errorComponent\`
The \`routeOptions.errorComponent\` option is a component that is rendered when an error occurs during the route loading or rendering lifecycle. It is rendered with the following props:
- \`error\` - The error that occurred
- \`reset\` - A function to reset the internal \`CatchBoundary\`
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
// Render an error message
return
{error.message}
},
})
\`\`\`
The \`reset\` function can be used to allow the user to retry rendering the error boundaries normal children:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
return (
{error.message}
)
},
})
\`\`\`
If the error was the result of a route load, you should instead call \`router.invalidate()\`, which will coordinate both a router reload and an error boundary reset:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
return (
{error.message}
)
},
})
\`\`\`
### Using the default \`ErrorComponent\`
TanStack Router provides a default \`ErrorComponent\` that is rendered when an error occurs during the route loading or rendering lifecycle. If you choose to override your routes' error components, it's still wise to always fall back to rendering any uncaught errors with the default \`ErrorComponent\`:
\`\`\`tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
if (error instanceof MyCustomError) {
// Render a custom error message
return
{error.message}
}
// Fallback to the default ErrorComponent
return
},
})
\`\`\`
# Data Mutations
Since TanStack router does not store or cache data, it's role in data mutation is slim to none outside of reacting to potential URL side-effects from external mutation events. That said, we've compiled a list of mutation-related features you might find useful and libraries that implement them.
Look for and use mutation utilities that support:
- Handling and caching submission state
- Providing both local and global optimistic UI support
- Built-in hooks to wire up invalidation (or automatically support it)
- Handling multiple in-flight mutations at once
- Organizing mutation state as a globally accessible resource
- Submission state history and garbage collection
Some suggested libraries:
- [TanStack Query](https://tanstack.com/query/latest/docs/react/guides/mutations)
- [SWR](https://swr.vercel.app/)
- [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)
- [urql](https://formidable.com/open-source/urql/)
- [Relay](https://relay.dev/)
- [Apollo](https://www.apollographql.com/docs/react/)
Or, even...
- [Zustand](https://zustand-demo.pmnd.rs/)
- [Jotai](https://jotai.org/)
- [Recoil](https://recoiljs.org/)
- [Redux](https://redux.js.org/)
Similar to data fetching, mutation state isn't a one-size-fits-all solution, so you'll need to pick a solution that fits your needs and your team's needs. We recommend trying out a few different solutions and seeing what works best for you.
> β οΈ Still here? Submission state is an interesting topic when it comes to persistence. Do you keep every mutation around forever? How do you know when to get rid of it? What if the user navigates away from the screen and then back? Let's dig in!
## Invalidating TanStack Router after a mutation
TanStack Router comes with short-term caching built-in. So even though we're not storing any data after a route match is unmounted, there is a high probability that if any mutations are made related to the data stored in the Router, the current route matches' data could become stale.
When mutations related to loader data are made, we can use \`router.invalidate\` to force the router to reload all of the current route matches:
\`\`\`tsx
const router = useRouter()
const addTodo = async (todo: Todo) => {
try {
await api.addTodo()
router.invalidate()
} catch {
//
}
}
\`\`\`
Invalidating all of the current route matches happens in the background, so existing data will continue to be served until the new data is ready, just as if you were navigating to a new route.
If you want to await the invalidation until all loaders have finished, pass \`{sync: true}\` into \`router.invalidate\`:
\`\`\`tsx
const router = useRouter()
const addTodo = async (todo: Todo) => {
try {
await api.addTodo()
await router.invalidate({ sync: true })
} catch {
//
}
}
\`\`\`
## Long-term mutation State
Regardless of the mutation library used, mutations often create state related to their submission. While most mutations are set-and-forget, some mutation states are more long-lived, either to support optimistic UI or to provide feedback to the user about the status of their submissions. Most state managers will correctly keep this submission state around and expose it to make it possible to show UI elements like loading spinners, success messages, error messages, etc.
Let's consider the following interactions:
- User navigates to the \`/posts/123/edit\` screen to edit a post
- User edits the \`123\` post and upon success, sees a success message below the editor that the post was updated
- User navigates to the \`/posts\` screen
- User navigates back to the \`/posts/123/edit\` screen again
Without notifying your mutation management library about the route change, it's possible that your submission state could still be around and your user would still see the **"Post updated successfully"** message when they return to the previous screen. This is not ideal. Obviously, our intent wasn't to keep this mutation state around forever, right?!
## Using mutation keys
Hopefully and hypothetically, the easiest way is for your mutation library to support a keying mechanism that will allow your mutations's state to be reset when the key changes:
\`\`\`tsx
const routeApi = getRouteApi('/room/$roomId/chat')
function ChatRoom() {
const { roomId } = routeApi.useParams()
const sendMessageMutation = useCoolMutation({
fn: sendMessage,
// Clear the mutation state when the roomId changes
// including any submission state
key: ['sendMessage', roomId],
})
// Fire off a bunch of messages
const test = () => {
sendMessageMutation.mutate({ roomId, message: 'Hello!' })
sendMessageMutation.mutate({ roomId, message: 'How are you?' })
sendMessageMutation.mutate({ roomId, message: 'Goodbye!' })
}
return (
<>
{sendMessageMutation.submissions.map((submission) => {
return (
{submission.status}
{submission.message}
)
})}
>
)
}
\`\`\`
## Using the \`router.subscribe\` method
See the [Router Events guide](./router-events.md) for a more complete walkthrough of the available events and when to use them.
For libraries that don't have a keying mechanism, we'll likely need to manually reset the mutation state when the user navigates away from the screen. To solve this, we can use TanStack Router's \`invalidate\` and \`subscribe\` method to clear mutation states when the user is no longer in need of them.
The \`router.subscribe\` method is a function that subscribes a callback to various router events. The event in particular that we'll use here is the \`onResolved\` event. It's important to understand that this event is fired when the location path is _changed (not just reloaded) and has finally resolved_.
This is a great place to reset your old mutation states. Here's an example:
\`\`\`tsx
const router = createRouter()
const coolMutationCache = createCoolMutationCache()
const unsubscribeFn = router.subscribe('onResolved', () => {
// Reset mutation states when the route changes
coolMutationCache.clear()
})
\`\`\`
# Deferred Data Loading
TanStack Router is designed to run loaders in parallel and wait for all of them to resolve before rendering the next route. This is great most of the time, but occasionally, you may want to show the user something sooner while the rest of the data loads in the background.
Deferred data loading is a pattern that allows the router to render the next location's critical data/markup while slower, non-critical route data is resolved in the background. This process works on both the client and server (via streaming) and is a great way to improve the perceived performance of your application.
If you are using a library like [TanStack Query](https://tanstack.com/query/latest) or any other data fetching library, then deferred data loading works a bit differently. Skip ahead to the [Deferred Data Loading with External Libraries](#deferred-data-loading-with-external-libraries) section for more information.
## Deferred Data Loading with \`Await\`
To defer slow or non-critical data, return an **unawaited/unresolved** promise anywhere in your loader response:
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async () => {
// Fetch some slower data, but do not await it
const slowDataPromise = fetchSlowData()
// Fetch and await some data that resolves quickly
const fastData = await fetchFastData()
return {
fastData,
deferredSlowData: slowDataPromise,
}
},
})
\`\`\`
As soon as any awaited promises are resolved, the next route will begin rendering while the deferred promises continue to resolve.
In the component, deferred promises can be resolved and utilized using the \`Await\` component:
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})
function PostIdComponent() {
const { deferredSlowData, fastData } = Route.useLoaderData()
// do something with fastData
return (
Loading...}>
{(data) => {
return
{data}
}}
)
}
\`\`\`
> [!TIP]
> If your component is code-split, you can use the [getRouteApi function](./code-splitting.md#manually-accessing-route-apis-in-other-files-with-the-getrouteapi-helper) to avoid having to import the \`Route\` configuration to get access to the typed \`useLoaderData()\` hook.
The \`Await\` component resolves the promise by triggering the nearest suspense boundary until it is resolved, after which it renders the component's \`children\` as a function with the resolved data.
If the promise is rejected, the \`Await\` component will throw the serialized error, which can be caught by the nearest error boundary.
# React
> [!TIP]
> In React 19, you can use the \`use()\` hook instead of \`Await\`
## Deferred Data Loading with External libraries
When your strategy for fetching information for the route relies on [External Data Loading](./external-data-loading.md) with an external library like [TanStack Query](https://tanstack.com/query), deferred data loading works a bit differently, as the library handles the data fetching and caching for you outside of TanStack Router.
So, instead of using \`defer\` and \`Await\`, you'll instead want to use the Route's \`loader\` to kick off the data fetching and then use the library's hooks to access the data in your components.
# React
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
// Fetch and await some data that resolves quickly
await queryClient.ensureQueryData(fastDataOptions())
},
})
\`\`\`
# Solid
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
// Fetch and await some data that resolves quickly
await queryClient.ensureQueryData(fastDataOptions())
},
})
\`\`\`
Then in your component, you can use the library's hooks to access the data:
# React
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})
function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions())
// do something with fastData
return (
Loading...}>
)
}
function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())
return
{data}
}
\`\`\`
# Solid
\`\`\`tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { useSuspenseQuery } from '@tanstack/solid-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})
function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions())
// do something with fastData
return (
Loading...}>
)
}
function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())
return
{data()}
}
\`\`\`
## Caching and Invalidation
Streamed promises follow the same lifecycle as the loader data they are associated with. They can even be preloaded!
# React
## SSR & Streaming Deferred Data
**Streaming requires a server that supports it and for TanStack Router to be configured to use it properly.**
Please read the entire [Streaming SSR Guide](./ssr.md#streaming-ssr) for step by step instructions on how to set up your server for streaming.
## SSR Streaming Lifecycle
The following is a high-level overview of how deferred data streaming works with TanStack Router:
- Server
- Promises are marked and tracked as they are returned from route loaders
- All loaders resolve and any deferred promises are serialized and embedded into the html
- The route begins to render
- Deferred promises rendered with the \`\` component trigger suspense boundaries, allowing the server to stream html up to that point
- Client
- The client receives the initial html from the server
- \`\` components suspend with placeholder promises while they wait for their data to resolve on the server
- Server
- As deferred promises resolve, their results (or errors) are serialized and streamed to the client via an inline script tag
- The resolved \`\` components and their suspense boundaries are resolved and their resulting HTML is streamed to the client along with their dehydrated data
- Client
- The suspended placeholder promises within \`\` are resolved with the streamed data/error responses and either render the result or throw the error to the nearest error boundary
# Document Head Management
Document head management is the process of managing the head, title, meta, link, and script tags of a document and TanStack Router provides a robust way to manage the document head for full-stack applications that use Start and for single-page applications that use TanStack Router. It provides:
- Automatic deduping of \`title\` and \`meta\` tags
- Automatic loading/unloading of tags based on route visibility
- A composable way to merge \`title\` and \`meta\` tags from nested routes
For full-stack applications that use Start, and even for single-page applications that use TanStack Router, managing the document head is a crucial part of any application for the following reasons:
- SEO
- Social media sharing
- Analytics
- CSS and JS loading/unloading
To manage the document head, it's required that you render both the \`\` and \`\` components and use the \`routeOptions.head\` property to manage the head of a route, which returns an object with \`title\`, \`meta\`, \`links\`, \`styles\`, and \`scripts\` properties.
## Managing the Document Head
\`\`\`tsx
export const Route = createRootRoute({
head: () => ({
meta: [
{
name: 'description',
content: 'My App is a web application',
},
{
title: 'My App',
},
],
links: [
{
rel: 'icon',
href: '/favicon.ico',
},
],
styles: [
{
media: 'all and (max-width: 500px)',
children: \`p {
color: blue;
background-color: yellow;
}\`,
},
],
scripts: [
{
src: 'https://www.google-analytics.com/analytics.js',
},
],
}),
})
\`\`\`
### Deduping
Out of the box, TanStack Router will dedupe \`title\` and \`meta\` tags, preferring the **last** occurrence of each tag found in nested routes.
- \`title\` tags defined in nested routes will override a \`title\` tag defined in a parent route (but you can compose them together, which is covered in a future section of this guide)
- \`meta\` tags with the same \`name\` or \`property\` will be overridden by the last occurrence of that tag found in nested routes
### \`\`
The \`\` component is **required** to render the head, title, meta, link, and head-related script tags of a document.
It should be **rendered either in the \`\` tag of your root layout or as high up in the component tree as possible** if your application doesn't or can't manage the \`\` tag.
For manifest-managed assets, you can also set \`crossorigin\` values on emitted
\`modulepreload\` and stylesheet links:
\`\`\`tsx
\`\`\`
\`assetCrossOrigin\` only applies to manifest-managed asset links emitted by Start.
If you also set \`crossOrigin\` via \`transformAssets\` (either the object shorthand
or a callback return value), \`assetCrossOrigin\` wins.
### Start/Full-Stack Applications
# React
\`\`\`tsx
import { HeadContent } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
),
})
\`\`\`
# Solid
\`\`\`tsx
import { HeadContent } from '@tanstack/solid-router'
export const Route = createRootRoute({
component: () => (
),
})
\`\`\`
### Single-Page Applications
First, remove the \`\` tag from the index.html if you have set any.
# React
\`\`\`tsx
import { HeadContent } from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: () => (
<>
>
),
})
\`\`\`
# Solid
\`\`\`tsx
import { HeadContent } from '@tanstack/solid-router'
const rootRoute = createRootRoute({
component: () => (
<>
>
),
})
\`\`\`
## Managing Body Scripts
In addition to scripts that can be rendered in the \`\` tag, you can also render scripts in the \`\` tag using the \`routeOptions.scripts\` property. This is useful for loading scripts (even inline scripts) that require the DOM to be loaded, but before the main entry point of your application (which includes hydration if you're using Start or a full-stack implementation of TanStack Router).
To do this, you must:
- Use the \`scripts\` property of the \`routeOptions\` object
- [Render the \`\` component](#scripts)
\`\`\`tsx
export const Route = createRootRoute({
scripts: () => [
{
children: 'console.log("Hello, world!")',
},
],
})
\`\`\`
### \`\`
The \`\` component is **required** to render the body scripts of a document. It should be rendered either in the \`\` tag of your root layout or as high up in the component tree as possible if your application doesn't or can't manage the \`\` tag.
### Example
# React
\`\`\`tsx
import { createRootRoute, Scripts } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => (
),
})
\`\`\`
# Solid
\`\`\`tsx
import { createFileRoute, Scripts } from '@tanstack/solid-router'
export const Route = createRootRoute('/')({
component: () => (
),
})
\`\`\`
## Inline Scripts with ScriptOnce
For scripts that must run before React hydrates (like theme detection), use \`ScriptOnce\`. This is particularly useful for avoiding flash of unstyled content (FOUC) or theme flicker.
# React
\`\`\`tsx
import { ScriptOnce } from '@tanstack/react-router'
const themeScript = \`(function() {
try {
const theme = localStorage.getItem('theme') || 'auto';
const resolved = theme === 'auto'
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
document.documentElement.classList.add(resolved);
} catch (e) {}
})();\`
function ThemeProvider({ children }) {
return (
<>
{children}
>
)
}
\`\`\`
# Solid
\`\`\`tsx
import { ScriptOnce } from '@tanstack/solid-router'
const themeScript = \`(function() {
try {
const theme = localStorage.getItem('theme') || 'auto';
const resolved = theme === 'auto'
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
document.documentElement.classList.add(resolved);
} catch (e) {}
})();\`
function ThemeProvider({ children }) {
return (
<>
{children}
>
)
}
\`\`\`
### How ScriptOnce Works
1. During SSR, renders a \`