# Use TanStack Query to Share Data between React Router Loaders

Used: react-router@7.0.2 and @tanstack/query-core@5.90.5

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.

```ts {% path="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.

```ts
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.

```ts {% path="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.

```tsx {% path="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.

```tsx {% path="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>
  );
}
```

```tsx {% path="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.
