DEV Community

Pranav Badami for Fyno

Posted on • Updated on

React Query - Maintaining Query cache

React Query has greatly simplified frontend API query handling with improved error management and efficient data caching. However, managing query keys can be challenging, as we discovered at Fyno when dealing with duplicated key names, leading to frustrating hours of debugging. This situation highlights the significance of effective key management. Duplicated keys can lead to data mix-ups and unexpected results. Lets delve into best practices for managing query keys, allowing you to harness React Query's full potential without the hassle of duplicated keys.

Old Approach

We create a react query hook for each API call and define the query key cache in the react query hook and if we want to invalidate some query then we had to go find the query key and use the same one. Which is a lot of manual work and when you are trying to be as productive as possible, this can cause issues which will be time consuming to debug.

Some examples of the old approach:

1 . Simple Query Key

import { useQuery } from 'react-query'

const getTemplates = () => {
 // make your API call here
 return response
}

export const useGetTemplates = () => {
  return useQuery(['templatesList'],() => getTemplates())
}
Enter fullscreen mode Exit fullscreen mode
import { useQuery } from 'react-query'

const getEvents = () => {
 // make your API call here
 return response
}

export const useGetEvents = () => {
  return useQuery(['eventsList'],() => getEvents())
}
Enter fullscreen mode Exit fullscreen mode

2 . Dynamic Query Key

import { useQuery } from 'react-query'

const getTemplateDetails = () => {
 // make your API call here
 return response
}

export const useGetTemplateDetails = (name, version) => {
  return useQuery(['templateDetails', name, version],() => getTemplateDetails())
}
Enter fullscreen mode Exit fullscreen mode
import { useQuery } from 'react-query'

const getEventDetails = () => {
 // make your API call here
 return response
}

export const useGetEventDetails = (name, version) => {
  return useQuery(['eventDetails', name, version],() => getEventDetails())
}
Enter fullscreen mode Exit fullscreen mode

Let's deep dive into the problem

Following the best practices, we've been using separate React Query hooks for each API call to the backend. However, each query call requires a unique query key to store the API response data, and as our project has grown with new features, we've encountered some challenges:

  • Query Key Conundrum: One of the dilemmas we faced is deciding what query key to assign to the cache for the response data from an API call. It may seem straightforward at first, but as your project evolves, making this decision can become quite a puzzle.

  • Invalidation and Refetching: Figuring out the right query key to invalidate or trigger a refetch based on another API call or a specific action can be like solving a complex puzzle. It's critical to ensure that your data remains up-to-date and accurate.

  • The Duplicity Dilemma: As we expanded our codebase, we encountered the issue of creating duplicate query keys. This happened when data from various API calls with different responses started to overlap, leading to confusion and unexpected results.

These challenges are not unique, and they can affect the efficiency and maintainability of your project. Lets explore strategies and techniques to tackle these issues head-on, ensuring a smoother development process and a more predictable and reliable application.


New Approach

To address these challenges, we took a strategic approach. Firstly, we centralized the creation of all query cache keys for easy accessibility and maintenance. Our goal was to make this process dynamic, allowing customization with specific values while maintaining a standardized approach.

Query Keys Factory

Our solution? We call it the "Query Keys Factory." Now, you might wonder why we went with such a name - well, sometimes, cool just sounds cool. It's a nifty little React hook, and its job is simple but incredibly valuable. This hook holds the keys to all the query keys, organized neatly according to the features within our product.

Create a simple global react hook, you can call it Query Keys Factory to be cool like us.

export const useQueryKeysFactory = () => {

  //--------Templates-----------> /templates
  const templateKeys = {
    list: ['templatesList'],
    details: (name, version) => ['templateDetails', name, version]
  }

//--------Events-----------> /events
  const templateKeys = {
    list: ['eventsList'],
    details: (name, version) => ['eventDetails', name, version]
  }

  return { templateKeys, eventKeys }
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we've showcased a straightforward example of a query cache for two API calls of two features, which in this case, are "Templates" and "Events". These API calls include a listing API, providing a list of all the templates/events, and a details API for retrieving information about a single template/event. What makes this approach especially powerful is the encapsulation of these keys within the templateKeys/eventKeys section. These keys serve as the foundation for all query keys related to the /templates and /events endpoint respectively.

By centralizing these keys in one place, you gain remarkable accessibility and maintainability. You can easily access them from anywhere in your application, making the management of query keys a breeze. To illustrate how it works in practice, let's consider the implementation of a React Query hook for retrieving the list of templates and events. This way, you can see how this approach seamlessly integrates into your development process, offering a cleaner, more organized way to handle query keys throughout your project.

1 . Simple Query Key

import { useQuery } from 'react-query'

const getTemplates = () => {
 // make your API call here
 return response
}

export const useGetTemplates = () => {
  const { templateKeys } = useQueryKeysFactory()

  return useQuery(templateKeys['list'],() => getTemplates())
}
Enter fullscreen mode Exit fullscreen mode
import { useQuery } from 'react-query'

const getEvents = () => {
 // make your API call here
 return response
}

export const useGetEvents = () => {
  const { eventKeys } = useQueryKeysFactory()

  return useQuery(eventKeys['list'],() => getEvents())
}
Enter fullscreen mode Exit fullscreen mode

2 . Dynamic Query Key

import { useQuery } from 'react-query'

const getTemplateDetails = () => {
 // make your API call here
 return response
}

export const useGetTemplateDetails = (name, version) => {
  const { templateKeys } = useQueryKeysFactory()

  return useQuery(templateKeys['details'](name, version),() => getTemplateDetails())
}
Enter fullscreen mode Exit fullscreen mode
import { useQuery } from 'react-query'

const getEventDetails = () => {
 // make your API call here
 return response
}

export const useGetEventDetails = (name, version) => {
  const { eventKeys } = useQueryKeysFactory()

  return useQuery(eventKeys['details'](name, version),() => getEventDetails())
}
Enter fullscreen mode Exit fullscreen mode

Now, let's dive into a straightforward yet powerful scenario: invalidating cache based on another API call. We'll walk you through creating a React Query hook for making a delete template/event request. If this deletion operation succeeds, we'll perform an act of cache magic – invalidating the listing API call's cached data. This little trick ensures that our application always fetches the freshest, most up-to-date data from the backend.

Here's a sneak peek at how it's done:

import { useMutation, useQueryClient } from 'react-query'

const deleteTemplate = template_name => {
  // Add your API call for deleting the template with name template_name
}

export const useDeleteTemplate = () => {
  const queryClient = useQueryClient()
  const { templateKeys } = useQueryKeysFactory()

  return useMutation(template_name => deleteTemplate(template_name), {
    onSuccess: () => {
       queryClient.invalidateQueries(templateKeys['list'])
       }
   })
}
Enter fullscreen mode Exit fullscreen mode
import { useMutation, useQueryClient } from 'react-query'

const deleteEvent = event_name => {
  // Add your API call for deleting the template with name event_name
}

export const useDeleteEvent = () => {
  const queryClient = useQueryClient()
  const { eventKeys } = useQueryKeysFactory()

  return useMutation(event_name => deleteTemplate(event_name), {
    onSuccess: () => {
       queryClient.invalidateQueries(eventKeys['list'])
       }
   })
}
Enter fullscreen mode Exit fullscreen mode

There you go – query invalidation made simple. No more hunting for query keys, just trust the Query Keys Factory to handle it for you. It's all about efficiency and letting the tools do the work.

In conclusion, our exploration of managing query keys with React Query has provided valuable insights. We've introduced the Query Keys Factory as a solution to simplify key management, streamline development, and minimize errors. With this approach, you can confidently handle query keys and optimize your development workflow. Mastering React Query's query keys can significantly improve your experience, making your projects more efficient and reliable. Happy coding!

Top comments (0)