Capítulo 24 de 24

Chapter 24: Monorepo Setup with React Query

Core Idea

This example extends the router/app monorepo split one step further: a dedicated post-query package owns TanStack Query's queryOptions() definitions, and the router package consumes them by holding a QueryClient in router context, letting route loaders (in yet another package or the router package itself) call queryClient.ensureQueryData(postQueryOptions(id)) without the router package needing to know how posts are fetched.

Setup

  • Routing style: file-based route tree in the router package (routeTree.gen.ts), same overall shape as ch022/ch023
  • Key dependencies: @tanstack/react-router, @tanstack/router-plugin, @tanstack/react-query, @tanstack/history, redaxios, zod, plus @router-mono-react-query/post-query as a workspace:* dependency of the router package
  • Structure: packages/router/src/router.tsx creates a QueryClient and passes it via router context. packages/post-query/src/postQueryOptions.tsx defines postQueryOptions(postId) using queryOptions() and a fetchPost helper, isolated in its own package so it can be reused or swapped independently of the router's routing logic.

Code Example

// packages/router/src/router.tsx
export const queryClient = new QueryClient()

export const router = createRouter({
  routeTree,
  context: { queryClient },
  defaultPendingComponent: () => (
    <div>Loading form global pending component...</div>
  ),
  // This makes the loader only wait 200ms before showing the pending component
  defaultPendingMs: 200,
  defaultPreload: 'intent',
  // Since we're using React Query, loader calls should never be considered stale
  defaultPreloadStaleTime: 0,
  scrollRestoration: true,
})

// packages/post-query/src/postQueryOptions.tsx
export const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ['posts', { postId }],
    queryFn: () => fetchPost(postId),
  })
  • What it demonstrates: pulling query-option factories into their own workspace package (post-query), separate from both the router package (route tree/loaders) and the app package (components), so query definitions can be versioned, tested, and reused independently of routing concerns.

Key Takeaways

  1. defaultPendingMs: 200 shortens how long the router waits before showing defaultPendingComponent, useful once loaders are doing real network work via React Query.
  2. Splitting query options into their own package (post-query) mirrors the same rationale as splitting routes from components in ch022: each concern gets its own package boundary and can be swapped or versioned independently.
  3. As in the basic React Query example (ch018), defaultPreloadStaleTime: 0 is set here too, this is a recurring pairing whenever a router's context.queryClient is the source of truth for freshness rather than the router's own preload cache.
  4. The QueryClient still lives in the router package (not a fourth package), it's the router's context wiring, closest in responsibility to createRouter itself.

Connects To

  • router-monorepo-simple (ch022) and router-monorepo-simple-lazy (ch023): this is the third step in the same monorepo progression, baseline split, then lazy-loaded components, then a dedicated query-options package. All three share the router/app package shape.
  • basic-react-query (ch018): the single-package version of the same ensureQueryData/queryOptions() pattern used here across a package boundary.