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.
app/lib/analytics.server.ts 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.
app/lib/query-context.ts 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.
app/root.tsx 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.
app/routes/dashboard.tsx 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> ); }
app/routes/dashboard._index.tsx 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.
Do you like my content?
Your sponsorship helps me create more tutorials, articles, and open-source tools.