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

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 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 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

The foundation of the pattern lives in 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.

// 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 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 file instantiates a single QueryClient with defaults optimized for static roadmap data and server-side rendering compatibility.

// 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 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.

// 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.

// 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 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 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 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →