DEV Community

Ulad Ramanovich
Ulad Ramanovich

Posted on

Mastering React Query. Structuring Your Code for Scalability and Reusability

Understanding React Query: The Basics and Benefits of Using It

React Query is a library that helps you manage your requests with caching. It simplifies how you can fetch data from the server and store it on the client side without needing additional state management tools like useState or redux.

const {data} = useQuery({
    queryKey: ['posts'],
    queryFn: () => {
        return getPosts()
    }
})
Enter fullscreen mode Exit fullscreen mode

React Query is excellent for both fetching and mutating data. By mutating most of the time we mean doing “non-GET” requests to update, delete, or create new entities. With useMutation, you can also simplify the logic for handling these types of requests.

const {mutate} = useMutation({
    mutationFn: updatePost,
})

const onSubmit = (post) => {
    mutate(post)
}
Enter fullscreen mode Exit fullscreen mode

When your project starts to grow and more developers begin reusing the same queries in different parts of the app, you may encounter typical issues common to most growing projects.

In every project I've seen, the same typical problems with React Query. often due to a luck of experience of because we tend to skip reading the full documentation. In this article, I'll discuss advanced React Query techniques based on my personal experience and share how you can create more efficient code with React Query.

Typical structure of react query usage

When you start work with react query it always two basic things:

  • Query keys
  • Query function
const {data} = useQuery({
    queryKey: ['posts'], // query key for query identification
    queryFn: () => { // query function for calling the API
        return fetchPosts()
    }
})
Enter fullscreen mode Exit fullscreen mode

Let’s talk about both of this important aspects and why we need to care about them more.

Query Keys

Query keys allow us to identify and store queries in the cache, making it crucial to structure your query keys thoughtfully to avoid unnecessary calls.

Another important use of keys is for cache invalidation. For example, if you have a list of articles on your blog and you add or modify a blog post, you’ll want to update all articles across all relevant queries.

Query function

This is part of the logic where we put our fetching logic. The query function also can decide when to throw an error in query if you have additional cases:

const getPosts = async () => {
    const response = await fetch('/api/posts')

    if (response.status !== 200) {
        throw new Error(response.error)
    }

    const data = await response.json()

    return data
}
Enter fullscreen mode Exit fullscreen mode

This means your query function can include logic and be reused between different useQuery calls.

Storing react query keys

In the documentation, you’ll find many examples where query keys are declared directly inside the useQuery hook. This approach can lead to several problems when you need to reuse the same key in different places.

The first think you can do is build a query keys factory for each entity:

const postsQueryKeys = {
    all: ['posts'],
    detail: (postId) => [...postsQueryKeys.all, 'detail', postId],
    list: (filters) => [...postsQueryKeys.all, 'list', {filters}]
}
Enter fullscreen mode Exit fullscreen mode

Every request should start with entity name, followed by what you want to fetch from this entity ("detail" or "list" in example). Now, imagine you have two different hooks with different parameters for this query:

const { data: publishedPosts } = useQuery({
  queryKey: postsQueryKeys.list({ limit: 10 }),
  queryFn: fetchPosts({ limit: 10 }),
});

const { data: unpublishedPosts } = useQuery({
  queryKey: postsQueryKeys.list({ limit: 5, favourites: true }),
  queryFn: () => fetchPosts({ limit: 5, favourites: true }),
});
Enter fullscreen mode Exit fullscreen mode

At first, this might seem like over-engineering, but when it comes to reusing your queries in different parts of your application, it becomes clear why this is an important step to optimize your codebase when working with React Query. You can store all query key factories in a file named query-keys.

One common use case for query keys factory when you need to call useMutation and than invalidate all queries with the specific keys:

const queryClient = useQueryClient()

const {mutation} = useMutation({
    queryFn: updatePost,
    onSuccess: (post) => {
        queryClient.invalidateQueries({ queryKey: postsQueryKeys.detail(post.id) })
    }
})
Enter fullscreen mode Exit fullscreen mode

Abstract for query function

When query keys could be used as part of the query factory let’s think how we can abstract query functions and why we need to do so.

If you take a look on example above you can see the we don’t abstract the logic of query functions and put them in the useQeury . The good practice here is also abstract all of the query functions as a separate functions. You can name them as resources .

const getPosts = async () => {
    const response = await fetch('/api/posts')

    if (response.status !== 200) {
        throw new Error(response.error)
    }

    const data = await response.json()
    const posts = data.posts

    return posts
}
Enter fullscreen mode Exit fullscreen mode

Now you can reuse resources in the different queries and test them separately from react-query logic.

Put all things together

Now that you have your query keys and functions set up for React Query, let's talk about how to organize and put everything together.

First and foremost is naming. I usually create an additional file named queries where I combine query keys with query functions to create reusable hooks.

For example:

import { useQuery } from 'react-query';
import { postsQueryKeys } from './query-keys';
import { fetchPosts } from './api';

export const usePostsQuery = (filters) => {
  return useQuery({
    queryKey: postsQueryKeys.list(filters),
    queryFn: () => fetchPosts(filters),
  });
};
Enter fullscreen mode Exit fullscreen mode

When naming reusable hooks, it's important to provide context for developers who may reuse them. It's a good practice to follow React Query's naming conventions by adding a query or mutation postfix. For example, usePostsQuery or usePostsMutation. This way, you can easily identify what the hook does, as the behavior of useQuery and useMutation is different.

Here’s an example:

// api.js
export const fetchPosts = async (filters) => {
  // Fetch logic for posts
};

// Another file where you use the custom hook
const { data: posts } = usePostsQuery({ published: true });
Enter fullscreen mode Exit fullscreen mode

By using descriptive names and consistent naming conventions, your code becomes more intuitive and easier to navigate, making it clear whether a hook is responsible for querying or mutating data.

Conclusion

Of course, if your project is small and doesn't require a lot of reusability between queries, you don't need to apply all of these suggestions. However, it's important to consider these practices before integrating React Query into a project where multiple developers will be working on it. By following these standards, you can avoid issues like double-fetching the same data and inconsistencies across different pages of your application.

Top comments (0)