React Query (Brief Introduction with Intermediate/Advanced Flash Notes)

React Query (Brief Introduction with Intermediate/Advanced Flash Notes)

Topics touched upon: React Query, useQuery, useMutation, cacheTime, staleTime, ReactQueryDevtools, refetch methods and cases, optimistic updates, etc.

Introduction

The goal of every developer is to create an experience that not only solves the problem but also does it effectively and intelligently. As far as user experience is concerned, one of the key factors to improvise it is to handle the state of the application.

React Query is a library that helps in fetching, caching, synchronizing, and updating the server state in a relatively cleaner code snippet than the traditional manner, which may also contain a lot of boilerplate code.
Furthermore, it also becomes easier for you to separate the server state code from the global state, thus the data is used only where required and cannot be accessed elsewhere.

Q. What makes React Query different? Are traditional libraries not sufficient?

A. The traditional state management libraries are great for handling the client-side states of the app, but they are not optimized for the server-side or asynchronous data.

They face the following challenges:

  • The data exists remotely and your app may not own or control it, thus the data may change without your supervision or even run out-of-date or "stale"
  • The API calls made for fetching and updating the state are async
  • Caching, invalidating, and reflecting updates to data becomes tricky and so does managing memory and garbage collection of server state
  • The complexity to improve performance optimizations like pagination and lazy loading due to uncertainty in data
  • Improving fetching concurrency and deduping multiple requests for the same data into a single request
    ...and many more

Thus, as mentioned in the definition at the top, React Query is a great (not just limited to) alternative to tackling such challenges. It comes with a couple of methods to make its usage specific to the use case. Let's discuss the two important ones here: useQuery and useMutation.

useQuery is a simple yet a detailed function that takes in mainly the queryKey (which will be hashed into a stable hash) and the queryFunction (that the query will use to request data) and returns the queried data, the real-time loading and fetching states, success and failure responses, along with many other responses that can be used to update the UI accordingly.

useQuery is ideally used to fetch the server-side data. Thus, another way of interacting with server state is to create/update/delete data or perform server side-effects. This is known as mutation, and React Query exports a useMutation hook for this purpose.

useMutation executes an asynchronous mutationFunction per se that can be used for easily making mutations from a React component. The hook can only be in 4 states: isIdle, isLoading, isError (returns error as a response) and isSuccess (returns data as a response).

With just variables, mutations aren't all that special, but when used with the onSuccess option, the Query Client's invalidateQueries method and the Query Client's setQueryData method, mutations become a very powerful tool.

Intermediate and Advanced Flash Notes and Snippets

  • Important utilities provided by React Query in a nutshell:
    QueryClient, QueryClientProvider, useQuery (isLoading, data, isError, error, isFetching, refetch (to manually trigger the query, e.g. onClick)), hasNextPage, fetchNextPage, isFetchingNextPage (InfiniteQueries which returns data.page instead of just data), useQueries (not "Query", allows DYNAMIC queries)

  • When useQuery fetches successfully : {onSuccess(data): function()} we can use select parameter to get specific data, or do data transformation (map, filter, etc..) right there.

  • When useQuery faces error during fetch (by default retries 3 times) : {onError(error): function()} Refetch means that the fetching will be done atleast once. To disable any fetch we add enabled: false to useQuery's third arguement.

  • Always wrap your app inside <ReactQueryDevtools> to visualize all of the inner workings of React Query which will likely save you hours of debugging. There are 4 statuses of query which are displayed by their query ID: fresh, fetching, stale, and inactive. It also shows what would things look like if seen in the Network tab of the browser's dev tools. You can also remember to use React Redux Toolkit for client-states.

  • cacheTime : isLoading DOES NOT update but isFetching DOES. (default: 5 minutes, if set to Infinity, will disable garbage collection) staleTime : isLoading DOES NOT update andisFetching DOES NOT update either. (default: 0 seconds, specifying a longer staleTime means queries will not refetch their data as often) where isLoading and isFetching are useQuery parameters and based on this we can render accordingly.

    After the query's cache time expires when the status of the query is stale, it is auto garbage collected.

  • Dependent on User Interactions (QueryClient) => refetchOnMount : Basically useEffect with [] as dependency (default: true or 'always') refetchOnWindowFocus : (Default: true or always) where always means irrespective of whether the query data is stale or not

  • Independent on User Interactions (useQuery) => refetchInterval: the query will automatically refetch, can be defined in milliseconds (default: false). However, this "polling" is paused if the window loses focus (we click elsewhere from the window), to still keep going, use "refetchIntervalInBackground: true".

Improve UX

  • {initialData: .... } when one query had multiple data objects and you saved that object in the cache, so if we click on one object, go back, and click on another object, the query data might already be present.

  • Pagination can also be done, and to KEEP previous data, we set keepPreviousData: true especially in pagination to avoid layout shift until the next page's data arrives. Similarly, for features like "Load More": use useInfiniteQuery, and object getNextPageParam(lastPage, pages) in its argument.

MUTATIONS

e.g., const {mutate, isLoading, isError, error} = useMutate(...)

  • Generally, it is a good idea to refetch the query automatically after successful mutation, we can use queryInvalidation via useQueryClient! e.g., onSuccess: ()=> queryClient.invalidateQueries('queryID')

  • Generally, the response that we get onSuccess returns us the data that was sent, so instead of refetching or increasing a network call we can just use this data to update the UI! So, to update the query cache, we use

onSuccess: (data) => {
     queryClient.setQueryData('queryID', (oldQueryData) => {
     //oldQueryData = what is present in query cache
     return {
     ...oldQueryData,
     data: [...oldQueryData.data, data.data]
     }

     })
}
  • The difference between using setQueryData and fetchQuery is that setQueryData is sync and assumes that you already synchronously have the data available. If you need to fetch the data asynchronously, it's suggested that you either refetch the query key or use fetchQuery to handle the asynchronous fetch.

  • Mutation has 3 callbacks:

    mutate(variables, {
     onError,
     onSettled,
     onSuccess,
    })
    
  • onMutate (same params as useMutation): first async-ly cancel all previous ongoing queries, and get old data using queryClient.getQueryData in case the errors come.

  • onError:

    (error, variablesInMutation, context - has additional info related to mutation) => {
       queryClient.setQueryData('queryKey', context.previousData - that we got in onMutate)
    }
    
  • onSettled: can be success or failure. Then refetch the data using invalidateQueries.

OPTIMISTIC UPDATES

Optimisting updates refer to updating the state on UI before it is confirmed on the backend! This means that we are optimistic that the backend operation will execute without any issues, so before the response arrives we update the UI and state beforehand.