# How to Integrate TanStack Query for Fetching Roadmap Data: A Production-Ready Pattern

> Discover the production-ready pattern for integrating TanStack Query to fetch roadmap data. Learn how pure functions, a singleton QueryClient, and custom hooks ensure deterministic caching and centralized error handling.

- Repository: [Kamran Ahmed/developer-roadmap](https://github.com/kamranahmedse/developer-roadmap)
- Tags: architecture
- Published: 2026-02-24

---

**The `developer-roadmap` repository implements a centralized query-options pattern where pure functions in `src/queries/` return configured `queryOptions` objects, a singleton `QueryClient` in [`src/stores/query-client.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/query-client.ts) manages global defaults, and custom hooks like `useCustomRoadmap` consume these definitions to fetch roadmap data with deterministic caching and centralized error handling.**

Integrating TanStack Query for fetching roadmap data effectively requires architectural decisions that separate data definitions from UI logic. The `kamranahmedse/developer-roadmap` project demonstrates a scalable approach that uses type-safe query options, a shared client configuration, and thin hook abstractions to manage roadmap JSON files, metadata, and user-specific content. This pattern ensures consistent caching behavior across server and client environments while keeping components free of data-fetching complexity.

## The Centralized Query-Options Architecture

The repository organizes TanStack Query integration into three distinct layers that promote reusability and testability. First, **query definition modules** in `src/queries/` export pure functions that return `queryOptions` objects containing the `queryKey`, `queryFn`, and default behaviors. Second, a **global QueryClient** in [`src/stores/query-client.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/query-client.ts) provides shared configuration for retries, refetching, and SSR guards. Third, **custom hooks** in `src/hooks/` import these options and the client, wrapping `useQuery` to inject environment-specific logic like URL construction and authentication tokens.

This architecture creates a unidirectional data flow: query options define *what* to fetch, hooks determine *when* and *how* to fetch, and components receive *ready-to-render* data.

## Defining Reusable Query Options in [`src/queries/roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/queries/roadmap.ts)

The foundation of the pattern lives in [`src/queries/roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/queries/roadmap.ts), where each data requirement exports a function returning a `queryOptions` object. These functions accept parameters (like `roadmapId`) and return a configuration object that TanStack Query consumes.

```typescript
// src/queries/roadmap.ts
export function roadmapJSONOptions(roadmapId: string) {
  return queryOptions({
    queryKey: ['roadmap-json', roadmapId],
    queryFn: async () => {
      const baseUrl = import.meta.env.PUBLIC_APP_URL;
      const roadmapJSON = await httpGet<RoadmapJSON>(`${baseUrl}/${roadmapId}.json`);
      const svg = renderFlowJSON(roadmapJSON);
      return { json: roadmapJSON, svg };
    },
    refetchOnMount: false,
  });
}

```

Key characteristics of this approach include **deterministic query keys** (arrays like `['roadmap-json', roadmapId]`) that enable automatic cache sharing across components, and **thin query functions** that wrap `httpGet` from [`src/lib/query-http.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/query-http.ts) to handle HTTP semantics. The `refetchOnMount: false` default prevents unnecessary network requests when valid cached data exists.

## Configuring the Global QueryClient

Centralized client configuration ensures consistent behavior across all roadmap queries. The [`src/stores/query-client.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/query-client.ts) file instantiates a single `QueryClient` with defaults optimized for static roadmap data and server-side rendering compatibility.

```typescript
// src/stores/query-client.ts
export const queryClient = new QueryClient({
  queryCache: new QueryCache({}),
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnMount: false,
      enabled: !import.meta.env.SSR,
    },
  },
});

```

This configuration disables automatic retries to prevent request storms, stops refetching on component mount to respect immutable roadmap JSON files, and guards against executing queries during server-side rendering with the `enabled` check against `import.meta.env.SSR`.

## Abstracting Data Fetching with Custom Hooks

The [`src/hooks/use-custom-roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/hooks/use-custom-roadmap.ts) file demonstrates how to consume the query options and global client. Rather than calling `useQuery` directly in components, this hook constructs dynamic query configurations based on props like `slug`, `id`, or `secret` tokens.

```typescript
// src/hooks/use-custom-roadmap.ts
export function useCustomRoadmap({ slug, id, secret }: UseCustomRoadmapOptions) {
  return useQuery<GetRoadmapResponse, FetchError>(
    {
      queryKey: ['get-roadmap', { slug, id }],
      queryFn: async () => {
        const url = slug
          ? `${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap-by-slug/${slug}`
          : `${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`;
        const roadmapUrl = new URL(url);
        if (secret) roadmapUrl.searchParams.set('secret', secret);
        return await httpGet<GetRoadmapResponse>(roadmapUrl.toString());
      },
      retry: false,
      enabled: !!(slug || id),
    },
    queryClient,
  );
}

```

The hook injects the global `queryClient` as the second argument to `useQuery`, ensuring all roadmap requests share the same cache and configuration. The `enabled` flag prevents queries from executing until required parameters are present, eliminating undefined URL errors.

## Consuming Queries in React Components

UI components remain focused on presentation by importing the custom hooks. The component receives `data`, `isLoading`, and `error` states without managing query keys or fetch logic.

```tsx
// Example component implementation
import { useCustomRoadmap } from '@/hooks/use-custom-roadmap';

export default function RoadmapPage({ slug }: { slug: string }) {
  const { data, isLoading, error } = useCustomRoadmap({ slug });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return <RoadmapRenderer roadmap={data!} />;
}

```

Because the query keys are deterministic and centralized, multiple components can call `useCustomRoadmap` with the same slug simultaneously without triggering duplicate network requests. TanStack Query automatically deduplicates these into a single fetch operation.

## Summary

- **Centralize query definitions** in `src/queries/` as pure functions returning `queryOptions` with explicit `queryKey` arrays and `queryFn` wrappers around `httpGet`.
- **Configure a singleton QueryClient** in [`src/stores/query-client.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/query-client.ts) with `retry: false`, `refetchOnMount: false`, and SSR guards to optimize for immutable roadmap data.
- **Abstract useQuery calls** into custom hooks like `useCustomRoadmap` that inject the global client, construct dynamic URLs, and handle parameter-based enabling.
- **Keep components pure** by having them consume hooks only for data, loading, and error states, leaving cache management and request deduplication to TanStack Query.

## Frequently Asked Questions

### Why use pure functions returning `queryOptions` instead of inline `useQuery` configurations?

Pure functions in [`src/queries/roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/queries/roadmap.ts) enable type-safe query key management and allow the same query configuration to be reused across multiple hooks or server-side prefetching routines. This separation ensures that changing a cache key or fetch logic requires updates in only one location, preventing inconsistencies between components that display the same roadmap data.

### How does the pattern handle server-side rendering (SSR) compatibility?

The global `QueryClient` in [`src/stores/query-client.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/query-client.ts) sets `enabled: !import.meta.env.SSR` in default query options, preventing hydration mismatches by disabling automatic fetches during server execution. This allows the application to safely instantiate queries during SSR without executing browser-specific fetch calls, while still hydrating cached data on the client.

### What is the purpose of the `enabled` flag in roadmap queries?

The `enabled` flag acts as a circuit breaker that prevents queries from executing until required parameters (like `slug` or `id`) are truthy, as implemented in `useCustomRoadmap`. This avoids requesting invalid URLs such as `/v1-get-roadmap/undefined` and allows components to mount safely before all props are available.

### Where is error handling implemented in this TanStack Query pattern?

Error handling is centralized in the custom hooks and the `httpGet` utility in [`src/lib/query-http.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/query-http.ts), which wraps fetch calls and throws typed `FetchError` instances. Components receive these errors through the `error` return value of `useQuery`, allowing for consistent error UI patterns without implementing try-catch blocks in every component.