Tanstack Query v5: Reusable Custom Hooks

Dec 13, 2023
- views
- likes

Since the release of TanStack Query v5, many people have faced issues with reusing custom hooks. In this article, I will show you how to solve this problem.

Problem

When you try to wrap your useQuery into a custom hook and define options as an argument with UseQueryOptions type, you will get an error when you try to pass some options in it:

Property 'queryKey' is missing in type '{ staleTime: number; refetchOnWindowFocus: false; }'
but required in type 'UseQueryOptions<Post, Error, Post, QueryKey>'.ts(2345)

Solution #1

You can Omit queryKey and queryFn from UseQueryOptions type like that:

import { type UseQueryOptions, useQuery } from '@tanstack/react-query';
import { type Post, getPost } from '@/entities/post';

export function usePost(
  id: number,
  options?: Omit<UseQueryOptions<Post>, 'queryKey' | 'queryFn'>,
) {
  return useQuery({
    queryKey: ['post', id],
    queryFn: () => getPost(id).then((response) => response.post),
    ...options,
  });
}

Now the error is gone and you can pass any options you want with autocompletion.

Solution #2

You can define in options specific ones you really needed like so:

import { type UseQueryOptions, useQuery } from '@tanstack/react-query';
import { type Post, getPost } from '@/entities/post';

export function usePost(
  id: number,
  options?: {
    // just a few for example
    staleTime?: UseQueryOptions<Post>['staleTime'];
    refetchOnWindowFocus?: UseQueryOptions<Post>['refetchOnWindowFocus'];
  },
) {
  return useQuery({
    queryKey: ['post', id],
    queryFn: () => getPost(id).then((response) => response.post),
    ...options,
  });
}

Solution #3: Query Options — My Favorite

Here is my favorite solution that I have been using for over a year and which has become my go-to approach.

This approach allows you to abstract away not only queryKey and queryFn, but also default settings (e.g., staleTime, gcTime) into a separate, reusable object, which can then be easily extended in the component.

Defining Query Options

Let's create a file, for example, posts/queries.ts, where query options related to posts will be stored.

// /post/queries.ts

import { type Post, getPost } from '@/entities/post';
import { type QueryOptions } from '@tanstack/react-query';

/**
 * Creates a QueryOptions object for fetching a specific post.
 * @param id The ID of the post.
 */
export const postQueryOptions = (id: number): QueryOptions<Post, Error> => ({
  queryKey: ['posts', 'post', id], // or postsKeys.post(id) - if you use a key convention
  queryFn: () => getPost(id).then((response) => response.post),
  staleTime: 300_000, // 5 minutes - example of a default value
  // You can add any other default parameters here (e.g., retry, gcTime)
});

Using Query Options with useQuery

Now, we can use this postQueryOptions object directly within the useQuery hook anywhere in the application, easily overriding or adding specific options.

// In a component

import { useQuery } from '@tanstack/react-query';
import { postQueryOptions } from '@/posts/queries';

const postId = 5;

// We spread the default options and add our own
const { data, isLoading, error } = useQuery({ // data is automatically typed as Post
  ...postQueryOptions(postId),
  // Here you can add your own parameters (e.g., select, enabled)
  // or override the default ones defined above (e.g., staleTime: 0)
  enabled: postId !== 0, // Example of adding a new option
});

What About Mutations?

The same principle works for Mutations (data changes)! Use MutationOptions to create reusable mutation configurations.

  1. Defining Mutation Options
// /post/mutations.ts

import { type MutationOptions } from '@tanstack/react-query';
import { createPost, type PostPayload, type Post } from '@/entities/post';

/**
 * Creates a MutationOptions object for creating a new post.
 */
export const createPostMutationOptions = ()  => ({
  mutationFn: (payload) => createPost(payload), // Your function to call the API
  // You can add default settings here, e.g., retry: 1
});

Using Mutation Options with useMutation

In the component, we simply spread the mutation options and add the necessary callbacks (onSuccess, onError) or other settings.

// In a component

import { useMutation } from '@tanstack/react-query';
import { createPostMutationOptions } from '@/posts/mutations';

const { mutateAsync: createPostMutate, isPending, error } = useMutation({
  ...createPostMutationOptions(),
  onSuccess: (data) => {
    console.log('Post created:', data.id);
    // e.g., You might invalidate the cache here
    // queryClient.invalidateQueries(['posts']);
  },
});

// Calling the mutation in your code
async function handleCreatePost() {
  try {
    const newPost = await createPostMutate({ title: 'First post!', content: 'Wooow!' });
    // Further logic
  } catch (err) {
    console.error('Creation error:', err);
  }
}

That's it! Now you can reuse your custom hooks and configurations with any options you want. The examples shown here are in React, but the concept also works for Vue, Solid, and other adapters.