Skip to content
TkDodo's blog

Context Inheritance in TanStack Router

Oct 12, 2025 — ReactJs, TanStack Router, TypeScript
Context Inheritance in TanStack Router
Photo by Janko Ferlič
TanStack Router

TanStack Router (opens in a new window) has a lot of great features, so it’s hard to pick favorites. That said, there is one thing that blew my mind once I saw it in action, and that is how the router lets you accumulate state between nested routes - not just at runtime, but also on type-level.

This feature works in a type-safe and fully inferred way for all parent-child route relations, but let’s start with the most simple one where I think you’d be surprised if that didn’t work:

Path Params

To show a quite minimal example, let’s just take two nested routes:

nested routes
- dashboard.$dashboardId
- route.tsx
- widget.$widgetId
- index.tsx

If we look at the child route (a widget on a dashboard), we can see that we get all params from our hierarchy back when calling Route.useParams():

useParams
export const
const Route: Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", undefined, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, AnyContext, AnyContext, AnyContext, {}, ... 5 more ..., undefined>
Route
=
createFileRoute<"/dashboard/$dashboardId/widget/$widgetId/", RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">(path?: "/dashboard/$dashboardId/widget/$widgetId/" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
(
'/dashboard/$dashboardId/widget/$widgetId/',
)({
UpdatableRouteOptionsExtensions.component?: RouteComponent
component
:
function Widget(): void
Widget
,
})
function
function Widget(): void
Widget
() {
const params =
const Route: Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", undefined, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, AnyContext, AnyContext, AnyContext, {}, ... 5 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">.useParams: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {}, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", undefined, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, ... 9 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseParamsBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useParams
()
const params: {
dashboardId: string;
widgetId: string;
}
}

This is literally what you’d expect from a type-safe router, after all, $dashboardId is right there in the path next to $widgetId, so why is this cool?

Well, what if we’d want to express that $dashboardId is a number? We would define that on the parent route by parsing the params with our favourite validation library:

params parsing
import {
type type<t = unknown, $ = {}> = [t] extends [" anyOrNever"] ? Type<t, $> : [t] extends [object] ? [t] extends [array] ? Type<t, $> : [t] extends [Date] ? Type<t, $> : Type<t, $> : [t] extends [string] ? Type<t, $> : [t] extends [number] ? Type<t, $> : Type<t, $>
const type: TypeParser<{}>
type
} from 'arktype'
export const
const Route: Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, {
dashboardId: number;
}, AnyContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
Route
=
createFileRoute<"/dashboard/$dashboardId", RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId">(path?: "/dashboard/$dashboardId" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/dashboard/$dashboardId')({
UpdatableRouteOptionsExtensions.component?: RouteComponent
component
:
const Dashboard: () => null
Dashboard
,
params?: {
parse?: ParseParamsFn<"/dashboard/$dashboardId", {
dashboardId: number;
}>;
priority?: number;
stringify?: StringifyParamsFn<"/dashboard/$dashboardId", {
dashboardId: number;
}>;
}
params
: {
parse?: ParseParamsFn<"/dashboard/$dashboardId", {
dashboardId: number;
}>
parse
:
type<{
readonly dashboardId: "string.integer.parse";
}, Type<{
dashboardId: (In: string) => To<number>;
}, {}>>(def: validateObjectLiteral<{
readonly dashboardId: "string.integer.parse";
}, {}, bindThis<{
readonly dashboardId: "string.integer.parse";
}>>): Type<{
dashboardId: (In: string) => To<number>;
}, {}> (+2 overloads)
type
({
dashboardId: "string.integer.parse"
dashboardId
: 'string.integer.parse' }).
Inferred<{ dashboardId: (In: string) => To<number>; }, {}>.assert: (data: unknown) => {
dashboardId: number;
}
assert
,
},
})

This change leads to something amazing: Every child route in the route tree now knows about this. The docs (opens in a new window) simply say that “once a path param has been parsed, it is available to all child routes”, but look what happens to our types:

dashboardId number
function
function Widget(): void
Widget
() {
const params =
const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, {
dashboardId: number;
}, AnyContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "widget/$widgetId", "/dashboard/$dashboardId/widget/$widgetId/", ... 13 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">.useParams: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {}, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, undefined, {}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, {
dashboardId: number;
}, AnyContext, AnyContext, AnyContext, ... 6 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseParamsBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useParams
()
const params: {
dashboardId: number;
widgetId: string;
}
}

The Widget now knows that the dashboardId is a number. Just like that. It knows. 🤯

Let that sink in for a minute, because it has some cool implications. Because if we can define things on the parent and have the children know about their types for path params, what stops us from applying the same concept to other state our router manages?

Nothing stops us, that’s what. And in fact, there are a couple of other places where this inheritance works as well:

Search Params

Yes, searchParams can inherit context on type-level from their parents, too. Let’s say we want to have an optional ?debug boolean flag available everywhere in our app. All we need to do is define it on our root component with:

Debug Search Param
export const
const Route: RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
Route
=
createRootRouteWithContext<RouteContext>(): <TRegister, TRouteContextFn, TBeforeLoadFn, TSearchValidator, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>(options?: RootRouteOptions<TRegister, TSearchValidator, RouteContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares, undefined> | undefined) => RootRoute<...>
createRootRouteWithContext
<
type RouteContext = {}
RouteContext
>()({
validateSearch?: Constrain<(data: unknown) => {
debug: boolean;
}, AnyValidator, DefaultValidator>
validateSearch
:
type<{
readonly debug: "boolean=false";
}, Type<{
debug: Default<boolean, false>;
}, {}>>(def: validateObjectLiteral<{
readonly debug: "boolean=false";
}, {}, bindThis<{
readonly debug: "boolean=false";
}>>): Type<{
debug: Default<boolean, false>;
}, {}> (+2 overloads)
type
({
debug: "boolean=false"
debug
: 'boolean=false' }).
Inferred<{ debug: Default<boolean, false>; }, {}>.assert: (data: unknown) => {
debug: boolean;
}
assert
,
component?: RouteComponent
component
:
const Root: () => null
Root
,
})

and now any component will get access to that boolean flag via useSearch:

useSearch
function
function Widget(): void
Widget
() {
const search =
const Route: Route<Register, RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", undefined, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, AnyContext, AnyContext, ... 7 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">.useSearch: <RouterCore<Route<Register, any, "/", "/", string, "__root__", (data: unknown) => {
debug: boolean;
}, {}, RouteContext, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", ... 12 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseSearchBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useSearch
()
const search: {
debug: boolean;
}
}

If we add more search params in our tree, they will be merged on type-level to produce the most accurate result. For example, our widget route might get a date range filter:

Merged Search Params
export const
const Route: Route<Register, RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", (data: unknown) => {
range?: "7d" | "30d" | "90d";
}, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, ... 9 more ..., undefined>
Route
=
createFileRoute<"/dashboard/$dashboardId/widget/$widgetId/", RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">(path?: "/dashboard/$dashboardId/widget/$widgetId/" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
(
'/dashboard/$dashboardId/widget/$widgetId/',
)({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, (data: unknown) => { debug: boolean; }, RouteContext, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.validateSearch?: Constrain<(data: unknown) => {
range?: "7d" | "30d" | "90d";
}, AnyValidator, DefaultValidator>
validateSearch
:
type<{
readonly 'range?': "\"7d\" | \"30d\" | \"90d\"";
}, Type<{
range?: "7d" | "30d" | "90d";
}, {}>>(def: validateObjectLiteral<{
readonly 'range?': "\"7d\" | \"30d\" | \"90d\"";
}, {}, bindThis<{
readonly 'range?': "\"7d\" | \"30d\" | \"90d\"";
}>>): Type<{
range?: "7d" | "30d" | "90d";
}, {}> (+2 overloads)
type
({ 'range?': '"7d" | "30d" | "90d"' }).
Inferred<{ range?: "7d" | "30d" | "90d"; }, {}>.assert: (data: unknown) => {
range?: "7d" | "30d" | "90d";
}
assert
,
UpdatableRouteOptionsExtensions.component?: RouteComponent
component
:
function Widget(): void
Widget
,
})
function
function Widget(): void
Widget
() {
const search =
const Route: Route<Register, RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", (data: unknown) => {
range?: "7d" | "30d" | "90d";
}, ResolveParams<"/dashboard/$dashboardId/widget/$widgetId/">, ... 9 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">.useSearch: <RouterCore<Route<Register, any, "/", "/", string, "__root__", (data: unknown) => {
debug: boolean;
}, {}, RouteContext, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, (data: unknown) => {
debug: boolean;
}, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/", ... 12 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseSearchBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useSearch
()
const search: {
range?: "7d" | "30d" | "90d";
debug: boolean;
}
}

But if we used useSearch on the dashboard route, we would only get access to the debug flag. This merging is insanely powerful, because it makes sure that every component gains access to all the state available throughout its parent route hierarchy. All it needs to do is to declare which route is used on.

Router Context

Another property of the Router that can do inheritance is the Router Context (opens in a new window). This context is created at the root route if we use createRootRouteWithContext, and it’s initial values are passed to createRouter itself. It is generally used for dependency injection into route loaders. When used with TanStack Query, we usually use it to distribute the QueryClient:

Router Context
const
const queryClient: QueryClient
queryClient
= new
new QueryClient(config?: QueryClientConfig): QueryClient
QueryClient
()
const
const router: RouterCore<RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
=
createRouter<RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>(options: RouterConstructorOptions<RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>): RouterCore<...>
createRouter
({
routeTree?: RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
routeTree
,
context: RouteContext
context
: {
queryClient: QueryClient
queryClient
,
},
Wrap?: (props: {
children: any;
}) => React.JSX.Element
Wrap
: ({
children: any
children
}) => {
return (
<
const QueryClientProvider: ({ client, children, }: QueryClientProviderProps) => React.JSX.Element
QueryClientProvider
client: QueryClient
client
={
const queryClient: QueryClient
queryClient
}>
{
children: any
children
}
</
const QueryClientProvider: ({ client, children, }: QueryClientProviderProps) => React.JSX.Element
QueryClientProvider
>
)
},
})

Then, this queryClient instance is available in all loaders:

QueryClient in Loader
export const
const Route: Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, RouteContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/dashboard/$dashboardId", RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId">(path?: "/dashboard/$dashboardId" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/dashboard/$dashboardId')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, RouteContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>>
loader
: async ({ context,
params: {
dashboardId: string;
}
params
}) => {
context: {
queryClient: QueryClient;
}
await
context: {
queryClient: QueryClient;
}
context
.
queryClient: QueryClient
queryClient
.
QueryClient.ensureQueryData<{}, Error, {}, string[]>(options: EnsureQueryDataOptions<{}, Error, {}, string[], never>): Promise<{}>
ensureQueryData
(
const dashboardQueryOptions: (dashboardId: string) => OmitKeyof<UseQueryOptions<{}, Error, {}, string[]>, "queryFn"> & {
queryFn?: QueryFunction<{}, string[], never>;
} & {
queryKey: string[] & {
[dataTagSymbol]: {};
[dataTagErrorSymbol]: Error;
};
}
dashboardQueryOptions
(
params: {
dashboardId: string;
}
params
.
dashboardId: string
dashboardId
),
)
},
UpdatableRouteOptionsExtensions.component?: RouteComponent
component
:
function Dashboard(): void
Dashboard
,
})
function
function Dashboard(): void
Dashboard
() {
const
const params: {
dashboardId: string;
}
params
=
const Route: Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, RouteContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId", "/dashboard/$dashboardId">.useParams: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, RouteContext, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, ... 9 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseParamsBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useParams
()
const {
const data: {}
data
} =
useSuspenseQuery<{}, Error, {}, string[]>(options: UseSuspenseQueryOptions<{}, Error, {}, string[]>, queryClient?: QueryClient): UseSuspenseQueryResult<{}, Error>
useSuspenseQuery
(
const dashboardQueryOptions: (dashboardId: string) => OmitKeyof<UseQueryOptions<{}, Error, {}, string[]>, "queryFn"> & {
queryFn?: QueryFunction<{}, string[], never>;
} & {
queryKey: string[] & {
[dataTagSymbol]: {};
[dataTagErrorSymbol]: Error;
};
}
dashboardQueryOptions
(
const params: {
dashboardId: string;
}
params
.
dashboardId: string
dashboardId
),
)
}

This is great, but Route Context is more than just dependency injection. Again, the docs state (opens in a new window) that “that you can modify the context at each route and the modifications will be available to all child routes.”

Route Context

To modify the context for a specific route, we can define the beforeLoad function and return whatever we want:

beforeLoad
export const
const Route: Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, AnyContext, AnyContext, () => {
readonly hello: "world";
}, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, RouteContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/dashboard/$dashboardId", RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId">(path?: "/dashboard/$dashboardId" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/dashboard/$dashboardId')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.beforeLoad?: Constrain<() => {
readonly hello: "world";
}, (ctx: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, undefined, ResolveParams<"/dashboard/$dashboardId">, AnyContext, AnyContext, "/dashboard/$dashboardId", unknown, undefined>) => any>
beforeLoad
: () => ({
hello: "world"
hello
: 'world' }) as
type const = {
readonly hello: "world";
}
const
,
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, RouteContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>>
loader
: async ({ context,
params: {
dashboardId: string;
}
params
}) => {
context: {
queryClient: QueryClient;
readonly hello: "world";
}
await
context: {
queryClient: QueryClient;
readonly hello: "world";
}
context
.
queryClient: QueryClient
queryClient
.
QueryClient.ensureQueryData<{}, Error, {}, string[]>(options: EnsureQueryDataOptions<{}, Error, {}, string[], never>): Promise<{}>
ensureQueryData
(
const dashboardQueryOptions: (dashboardId: string) => OmitKeyof<UseQueryOptions<{}, Error, {}, string[]>, "queryFn"> & {
queryFn?: QueryFunction<{}, string[], never>;
} & {
queryKey: string[] & {
[dataTagSymbol]: {};
[dataTagErrorSymbol]: Error;
};
}
dashboardQueryOptions
(
params: {
dashboardId: string;
}
params
.
dashboardId: string
dashboardId
),
)
},
UpdatableRouteOptionsExtensions.component?: RouteComponent
component
:
const Dashboard: () => ReactElement
Dashboard
,
})

Whatever we return will not only be available to this route’s loader, but also wherever we consume the context in child routes, for example, with useRouteContext:

useRouteContext
function
function Widget(): void
Widget
() {
const { hello } =
const Route: Route<Register, Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, AnyContext, AnyContext, () => {
readonly hello: "world";
}, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 15 more ..., undefined>
Route
.
RouteExtensions<"/dashboard/$dashboardId/widget/$widgetId/", "/dashboard/$dashboardId/widget/$widgetId/">.useRouteContext: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, RouteContext, AnyContext, AnyContext, {}, undefined, readonly [Route<Register, RootRoute<Register, undefined, RouteContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", "/dashboard/$dashboardId", undefined, ResolveParams<"/dashboard/$dashboardId">, ... 9 more ..., undefined>], unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown>(opts?: UseRouteContextBaseOptions<...> | undefined) => {
...;
}
useRouteContext
()
const hello: "world"
}

Okay, so the context can also evolve and inherit values from its parents, but where would that be useful? Of course we can use it for things like building generic breadcrumbs (opens in a new window), but I think I’ve found a killer use-case for React Query as well that I will be writing about in the next blog post.


That’s it for today. Feel free to reach out to me on bluesky (opens in a new window) if you have any questions, or just leave a comment below. ⬇️

Like the monospace font in the code blocks?

Check out monolisa.dev

Bytes - the JavaScript Newsletter that doesn't suck