Global state management combined with fetching data has always been a problem to crack in react applications. The most common patterns that we observe are of making use of React-Redux with asynchronous actions creators using middleware like thunk, and either fetching data in the useEffect
hook or the solutions provided by React-Redux like Connect HOC or useSelector
/useDispatch
hooks.
In this blog, we will explore another very appealing and new option that is React-Query. To get it installed in a React app using:
And we’ll have to wrap our root component with QueryClientProvider
and supply it with an instance of QueryClient
. A very familiar setup if you have worked with Redux before in react because they both make use of the provider pattern. We can also initiate React query dev tools in it (recommended).
We will walk through from making basic queries to a bit more advanced concepts.
The biggest difference we observe while using react-query is that it manages all the stages involved in the fetching cycle for which we dispatch multiple actions in redux. React-query provides us a useQuery
hook, which returns fields like isLoading
, isError
to cater to the fetching cycle. The useQuery hook takes in three arguments:
- Query Key
- Query Function
- Options Object
Query Key:
This has to be a unique key for a response of an API. React-query uses this to serialize and cache the response data by an API endpoint. This cache is accessible globally in the app. If we request again using the same query, it won’t trigger a re-fetch and bring the data from the global cache, unless the query has been invalidated (more on this later).
Query Function:
This is a basic function that could use either fetch or Axios to request data from an endpoint.
Options Object:
This is passed as the third argument to the useQuery
hook. It contains all the configs as to how the query should behave, and when it will have to re-fetch again from the server.
A sample query:
React-Query Mechanism:
So, react query works by displaying the cached data and then running a re-fetch in the background. It is because the default stale time is set to 0 seconds. It re-fetches the data and compares it with cached data, if there are any differences, new data is plugged in. We can however change this functionality if we are sure our data does not update very frequently. We need to set stale time to longer than 0 seconds, and so we can use this cached data in other components as well. We can see in the code above as well, that we get isFetching
flag to determine if a background fetch is in process or not. Additionally, useQuery
also returns a function refresh()
which we can use to re-fetch based on user events.
The very first thing to observe here is that when the component UserListingOne
mounts, the API response is cached in the query key ‘user-listing’. The other component UserListingTwo
also includes a useQuery
hook with the same query key. So, when once the data has been cached, it is not refetched when we go to UserListingTwo
from UserListingOne
.
Options Object Most used Fields:
enabled
: by default its true, but can be set to false so that the query wont execute. Query can then be triggered conditionally. Also helpful in querying with multiple params and not being sure if a parameter is available while querying or not. In such cases enabled could be conditionally set to false.
cacheTime
: Takes time in milliseconds, caches data for that particular time. Default duration is set to 5mins. It is to note though, that to determine if the cached data is not stale, react query will run background fetch on every re-render of component.
staleTime
: The time it would take for our data to be considered stale. React query won’t run background re-fetches until stale time is exceeded for data, and that would be triggered on a re-render of the component.
refetchOnMount
: takes in either of three values, ‘’always”, true, false. Re-fetches data when the component mounts.
refetchOnWindowFocus
: when a tab has been out of focus and is brought into focus again, this can be used to trigger a re-fetch.
refetchInterval
: time in milliseconds and fetches the data after the interval has passed. Use full where data needs to be updated consistently to keep the UI updated.
refetchIntervalBackground
: takes in a Boolean value, and specifies if the refetching by interval should continue even in the background.
keepPreviousData
: helpful in paginated queries, with it a field is returned by query `isPreviousData
`.
onSuccess(data)
: takes a function, to run an effect after API has successfully returned a response.
onError(data)
: takes a function, to run an effect after API has failed to return a response.
select(data):
takes a function, where API response is available as data. We can transform data, add more to it, delete from it, or serialize it however we need. The data returned from this function will be returned as data by useQuery
hook itself.
How to maintain cache using dynamic query keys?
As much functionality the options argument provides us these fields, we are still to understand how to re-fetch data depending on state variables. If a state variable has been updated and we would want to re-fetch or supply the new query params from the state-variable this is how we would do it. It’s akin to how we fetch data in useEffect
based on state variables in the dependency array.
What actually happens here is that we create new keys with this data which are also cached and made available globally. Anytime the product listing with the same set of filters will be requested the cached data will be returned if that data is not stale.
Here the users are cached with their respective IDs in the database because the query key was [“user-by-id”, userId]
. Once a query has been cached of the user with their user id, the next time the same user id will request, react query won’t need to re-fetch. This is how we can add variables to specify what we have fetched and cache it. However, if we have more than one variable, let’s say userId and userStatus, its recommended to keep an object as a query key instead of individual variables, because the order of variables in the array matter, but the object would be compared with the keys it contains in any order.
Query Client:
Reading and Updating Cache:
Imagine a scenario where we have cached a listing and then we try to mutate it either by adding, deleting, or updating the data in the listing. In this case, we will have to re-fetch the listing after the update, but that nullifies the reason for caching and using react-query as a global store. We can instead update the cache by using setQueryData
. This receives the query key of the query that we wish to update and a callback function that get the cached state as an argument. We have to be careful in what we return from this function because whatever these callback returns is set as the cache of that particular query. Similarly QueryClient
another function, getQueryData
which
Invalidating Queries:
Not always we will need to rely on stale time provided to the useQuery
hook. To invalidate the stale time in query would mean we demand fresh data from it. We need to access the instance of the queryClient
and call the function invalidateQueries
on it. We need to supply the query key in arguments which we want to invalidate.
Conclusion:
React query is a very efficient query caching client for the front-end which can replace traditional methods of fetching data on the mounting stage of the component and global state management like Redux/MobX in some use cases.
Thanks for reading, Don’t forget to share your thoughts in comment section!