Used
How toUse TanStack Query to Share Data between React Router Loaders
Let's say you have two routes that match the same URL, e.g. app/routes/dashboard and app/routes/dashboard._index, and both need to display user statistics for a dashboard.
The traditional way is that you get the data on both loaders, even if that means you fetch it two times.
import { z } from "zod";
export const UserStatsSchema = z.object({
totalUsers: z.number(),
activeUsers: z.number(),
newUsersToday: z.number(),
completionRate: z.number(),
});
export async function fetchUserStats() {
let response = await fetch("https://api.example.com/analytics/users");
return UserStatsSchema.promise().parse(response.json());
}
But we could do something better, we can implement an in-memory server-side cache to share data.
import { cache } from "~/cache.server";
export async function fetchUserStats() {
if (cache.has("user-stats")) return cache.get("user-stats");
let response = await fetch("https://api.example.com/analytics/users");
let stats = await UserStatsSchema.promise().parse(response.json());
cache.put("user-stats", stats);
return stats;
}
The problem is that if the two loaders trigger fetchUserStats at the same time both will get cache.has("user-stats") as false.
So we also need a way to batch and dedupe requests.
Enters TanStack Query.
This library has a QueryClient object that can cache the data of the queries for us, and if the same query is executed twice it will only run it once.
And a great thing about that library is that like there's a React version there's also @tanstack/query-core which is framework agnostic, so we can use it fully server-side without using the React hooks.
Create a Query Context
First, we need to create a context to share the QueryClient instance across our routes using React Router's context API.
import { createContext } from "react-router";
import type { QueryClient } from "@tanstack/query-core";
export const queryClientContext = createContext<QueryClient>();
Add QueryClient Middleware to Root Route
Add the middleware directly to your root route so it's available to all child routes.
import { QueryClient } from "@tanstack/query-core";
import { queryClientContext } from "./lib/query-context";
import type { Route } from "react-router";
import { Outlet } from "react-router";
export const middleware: Route.MiddlewareFunction[] = [
async ({ context }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Cache data indefinitely for the duration of the request
staleTime: Number.POSITIVE_INFINITY,
},
},
});
context.set(queryClientContext, queryClient);
},
];
Use the QueryClient in Parent and Child Routes
Now we can use the QueryClient in routes that match the same URL. Let's create a parent route and a child route that both need the same user statistics for the dashboard.
import type { Route } from "react-router";
import { Outlet, useLoaderData } from "react-router";
import { queryClientContext } from "~/lib/query-context";
import { fetchUserStats } from "~/lib/analytics.server";
export async function loader({ context }: Route.LoaderArgs) {
const queryClient = context.get(queryClientContext);
const userStats = await queryClient.fetchQuery({
queryKey: ["user-stats"],
queryFn: fetchUserStats,
});
return { userStats };
}
export default function Dashboard() {
const { userStats } = useLoaderData<typeof loader>();
return (
<div>
<h1>Analytics Dashboard</h1>
<div className="stats-overview">
<div>Total Users: {userStats.totalUsers}</div>
<div>Active Users: {userStats.activeUsers}</div>
</div>
<Outlet />
</div>
);
}
import type { Route } from "react-router";
import { useLoaderData } from "react-router";
import { queryClientContext } from "~/lib/query-context";
import { fetchUserStats } from "~/lib/analytics.server";
export async function loader({ context }: Route.LoaderArgs) {
const queryClient = context.get(queryClientContext);
const userStats = await queryClient.fetchQuery({
queryKey: ["user-stats"],
queryFn: fetchUserStats,
});
return { userStats };
}
export default function DashboardIndex() {
const { userStats } = useLoaderData<typeof loader>();
return (
<div>
<h2>Detailed Analytics</h2>
<div className="detailed-stats">
<p>New users today: {userStats.newUsersToday}</p>
<p>Completion rate: {userStats.completionRate}%</p>
<div className="chart">
{/* Chart component would go here */}
</div>
</div>
</div>
);
}
With this setup, when a user visits /dashboard, both the parent dashboard.tsx and child dashboard._index.tsx loaders will run in parallel. Since they both use the same queryKey of ["user-stats"], TanStack Query will only execute the fetchUserStats function once and share the cached result between both routes.