Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

React Query Hooks in Server-Side Loaders

Hello! Welcome back.

In our last lesson, we implemented the full React Query hydration flow—prefetching on the server, dehydrating the cache, and rehydrating on the client. This robust pattern allows us to efficiently pass server-fetched data to our components. So far, we've been using manually created data fetching functions.

Today, we'll elevate our architecture by automating the creation of our data-fetching layer. This lesson addresses the following learning outcome:

Integrate OpenAPI-generated React Query hooks within server-side route loaders.

We will learn how to use an OpenAPI specification—the contract for our API—to automatically generate fully type-safe React Query hooks. This eliminates boilerplate, prevents frontend-backend drift, and significantly speeds up development, all of which are crucial for building and maintaining a large-scale application.

1. The "Why": From Manual Fetching to a Generated Client

In any large project, manually writing and maintaining API fetching functions and their associated TypeScript types is tedious and error-prone.

  • A backend developer changes an endpoint, and the frontend breaks at runtime.
  • A new parameter is added, and you have to manually update types across your codebase.
  • Different developers might implement fetching logic inconsistently.

The solution is to adopt a "single source of truth" for your API's structure: an OpenAPI specification. OpenAPI (formerly known as Swagger) is a standard, language-agnostic format for describing RESTful APIs. By using a tool to generate a data-fetching client directly from this specification, we gain several powerful advantages.

The following article provides an excellent overview of the benefits.

Save time and improve consistency by generating React ...

This article, "Save time and improve consistency by generating React Query with OpenAPI" from the M-Way Solutions Engineering blog, perfectly captures the motivation behind API client generation. It details the real-world problems of manual API integration and the benefits of an automated approach.

Please read the introduction and the section titled "Cue, API gen. But, what does it really give us? 🤔". Throughout this initial overview, focus on the list of benefits, such as type-safety, FE/BE sync, and auto-complete.

As the article highlights, this approach gives us type safety, automatic synchronization with the backend, and out-of-the-box features that streamline development—all key concerns for a lead developer.

2. Choosing and Configuring a Generation Tool: Orval

Several tools can generate clients from an OpenAPI spec. We'll focus on Orval because it has excellent support for TanStack Query and, crucially for our SSR architecture, can generate the server-side prefetching helpers we need.

The process involves three main steps:

  1. Obtain the OpenAPI specification from your backend (usually a spec.json file).
  2. Configure Orval to generate TanStack Query hooks.
  3. Run the generation command.

Configuration

After installing Orval (npm install orval -D), you create a configuration file, orval.config.js. The configuration tells Orval where to find your API spec and how to generate the client code.

A key part of the configuration is the output.client and output.override.query sections. This is where we instruct Orval to generate code specifically for TanStack Query, including the server-side prefetch functions.

Save time and improve consistency by generating React ...

Let's return to the same article to see how to configure Orval. The author explains their choice of library and provides a complete configuration file.

Please read the sections "Choosing a generation library" and "Let’s get generating 🏃". Pay close attention to the orval.config.js example. Specifically, notice the client: "react-query" setting and the query object with usePrefetch: true. This usePrefetch flag is what enables the SSR pattern we're implementing.

Here is a simplified version of the configuration from the article, adapted for our context:

// orval.config.js
module.exports = {
  api: { // We can name our config block
    output: {
      // Generate code for TanStack Query
      client: 'react-query',
      // We'll place our generated files in 'src/api/generated'
      workspace: 'src/api/generated',
      // Generate separate files for models (types) and services (hooks)
      mode: 'tags-split',
      schemas: 'models',
      target: 'services',
      override: {
        query: {
          // Generate client-side hooks (e.g., useGetPosts)
          useQuery: true,
          // Generate server-side prefetch helpers (e.g., prefetchGetPosts)
          usePrefetch: true, 
        },
      },
    },
    input: {
      // URL or local path to your OpenAPI v3 spec
      target: 'http://localhost:8000/api-json',
    },
  },
};

With this config, running npx orval would read your API spec and generate a set of files in src/api/generated, including type-safe React Query hooks for every GET endpoint.

3. Integrating Generated Hooks into the SSR Flow

Now, let's refactor the code from our previous lesson to use these newly generated hooks. This is where the true power of this pattern becomes apparent.

Step 1: Refactor the Route Loader

Recall our previous loader, where we manually specified the queryKey and queryFn:

// Previous loader (manual)
import { fetchPosts } from '../utils/api';

// ...
loader: async () => {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
  return { dehydratedState: dehydrate(queryClient) };
},
// ...

With Orval, this becomes much cleaner. Assuming our API has an endpoint to get posts, Orval would generate a prefetchGetPosts function.

// New loader (with generated hook)
import { dehydrate, QueryClient } from '@tanstack/react-query';
// Import the generated prefetch helper
import { prefetchGetPosts } from '../api/generated/posts/posts';

// ...
loader: async ({ search }) => { // Assuming we can pass search params
  const queryClient = new QueryClient();

  // The generated function encapsulates the queryKey and fetcher.
  // We just pass the queryClient and any API parameters.
  await prefetchGetPosts(queryClient, { page: search.page || 1 });

  return { dehydratedState: dehydrate(queryClient) };
},
// ...

The prefetchGetPosts function, created by Orval, is a simple wrapper around queryClient.prefetchQuery. It automatically constructs the correct queryKey based on the endpoint and parameters (['/posts', { page: 1 }]) and calls the generated, type-safe fetching function. This eliminates manual work and potential errors.

Step 2: Refactor the Component

The client-side component is similarly simplified. Instead of a manual useQuery call, we use the generated useGetPosts hook.

// Previous component (manual)
function PostList() {
  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
  // ... render posts
}

The refactored component is cleaner and fully type-safe.

// New component (with generated hook)
import { useGetPosts } from '../api/generated/posts/posts';
import { Route } from '../routes/posts'; // Assuming this is the route file

function PostList() {
  // Get search params from the router to ensure they match the loader
  const { page } = Route.useSearch();

  // The generated hook takes the same parameters as the prefetch function.
  // React Query will find the prefetched data in the cache via the matching query key.
  const { data: postsResponse } = useGetPosts({ page: page || 1 });

  // The response 'data' is also fully typed according to the OpenAPI spec!
  return (
    <ul>
      {postsResponse?.data.items.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Crucially, the parameters passed to prefetchGetPosts in the loader and useGetPosts in the component must match. This is how React Query correctly resolves the data from the hydrated cache using the query key. Passing route search parameters through is a common way to ensure they are in sync.

The article we've been referencing demonstrates this exact server-side pattern.

Save time and improve consistency by generating React ...

Let's look at the article one last time to see the server-side implementation. This confirms the pattern we just discussed.

Please read the short section titled "Server side". Notice how it calls prefetchGetHats(queryClient, filters), which is identical to our prefetchGetPosts example. The author also correctly points out the importance of matching filters (parameters) between server and client.

4. Handling Authentication

A real-world application needs to handle authenticated requests. This is where Orval's mutator feature comes in. A mutator is a function that wraps the actual API call, allowing you to inject custom logic, such as adding an Authorization header.

The article provides a great example of a mutator that handles auth differently on the server (using getServerSession) versus the client (using getSession). While we will cover authentication in detail in a future lesson, it's valuable to see how the generated client is designed to accommodate it.

Save time and improve consistency by generating React ...

This section of the article shows how to create a custom 'mutator' to handle API request logic, such as adding authentication tokens. This is a critical piece for any production application.

Quickly scan the code example in the section that begins with "The mutator section might have caught your eye." Notice the if (typeof window === 'undefined') check. This is the standard way to differentiate between server and client environments in an SSR app to fetch session data accordingly.

By configuring Orval to use this mutator, every single generated API call will automatically include the necessary authentication headers, whether it's executed on the server in a loader or on the client in a component.

Conclusion

Today, we've bridged the gap between our API contract and our application's data-fetching layer. By leveraging OpenAPI and a code generator like Orval, we've created a robust, type-safe, and maintainable system for fetching data in an SSR context.

Key Takeaways:

  • OpenAPI as a Contract: Using an OpenAPI specification as the single source of truth for your API prevents inconsistencies between the frontend and backend.
  • Code Generation with Orval: Tools like Orval can read an OpenAPI spec and generate type-safe TanStack Query hooks, including server-side prefetch helpers.
  • SSR Integration: The generated prefetch... functions are used in Tanstack Router's loader to populate the server-side query cache for hydration.
  • Simplified Code: This pattern dramatically simplifies both server and client code, removing manual queryKey and queryFn definitions and replacing them with clean, type-safe hooks.
  • Consistency is Key: The parameters passed to the prefetch function on the server and the useQuery hook on the client must be identical for hydration to work correctly.

Next Up:

We've established a powerful, type-safe data-fetching pipeline. The next logical step is to properly secure it. In our next lesson, we will dive deep into authentication: Implement cookie-based session management for persistent authentication in an SSR context.

Can't find a good explanation? Sign up and we'll make it for you

Sign up