Asynchronous State Management With React-Query
Wrapping our requests in useQuery
and using the cache key means that we don’t really need to think about state management at all for the data we receive from network requests

Take a moment to think of the shared state in your latest React project. For most apps, this may include the currently logged-in user.
A blog application may include all available blog posts or all the blog posts written by a specific author.
A fitness app may include a list of exercises for a selected muscle group.
A baseball app may include the batting statistics for a specific player. Really, the shared state in our React apps can be just about anything!
But what do all of these pieces of state typically have in common? We access them via network requests (either to our own backend or to a third-party API).
Let’s take a look at how we can use custom hooks and react-query to manage these pieces of state that come from asynchronous network requests.
Leverage the useQuery Hook
In our example, we’ll be heavily using the useQuery()
hook from react-query. This hook takes three arguments —
- a cache key for the request we’ll make
- a query function
- an options object.
For all our examples, let’s imagine a fitness app that allows users to search for exercises by muscle group. First, let’s get a thorough understanding of how we can use useQuery()
to make a network request that gets all back exercises from our imaginary API.
First, we’ve defined our query function, fetchBackExercises()
, which makes a request to our imaginary endpoint, /exercises?muscleGroup=back
.
We can imagine that this endpoint will return a list of exercise objects as JSON that can be used in our react application to to create a component that lists the exercises.
Honestly, this looks a little messy to me. I like my components to be as readable as possible, and there’s a whole lot of JavaScript in there that makes it too tough to know what’s happening with a single glance. Let’s refactor a bit to put all of the query logic into a custom hook.
This is looking better! All of the logic around the network request for fetching exercises is now encapsulated in our custom useExercises()
hook.
The hook takes one argument, muscleGroup
, so it can be reused anywhere in our app easily.
The muscleGroup
arg is used both in the query param in the actual network request, and is also used in the cache key of the useQuery
hook, which will become important shortly.
This allows us to simplify our <ExerciseList />
component so that all we’re doing is calling our hook, and mapping the result to create our list of components.
How Does This Help With State Management?
So far we’ve got a pretty clean, reusable hook and component, but what does this have to do with state management? The answer lies in react-query’s caching.
Remember that the first argument of the useQuery()
hook is a cache key. The response of every network request made through useQuery()
is saved in a cache using that key.
Before making a request, react-query will check the cache. If the same key already exists in the cache, useQuery()
will simply grab that cached response and return it, instead of wasting resources on making the same request again.
Essentially, this means that react-query’s cache becomes our data store for any state coming from network requests (which for most apps is pretty much everything!).
In our example of a useExercise()
hook, many differnent components may use the hook to request exercises for muscleGroup: 'back'
. The actual network request will only be made once, and every subsequent call of useExercises('back')
will simply grab the response out of the react-query cache.
The same is true for any other muscle group we request ( useExercises('chest')
, useExercises('biceps')
, useExercises('legs')
, etc). Since we pass the muscleGroup
argument into the cache key ( ['exercises', muscleGroup]
), each new muscleGroup
will trigger an actual network request, but each subsequent request will use the cache as if it’s just any other piece of state.
So what are the benefits of managing our async state this way? Wrapping our requests in useQuery
and using the cache key means that we don’t really need to think about state management at all for the data we receive from network requests.
We don’t need to worry about context/providers, and we definitely don’t need to worry about any third-party state management libraries. Managing async state this way allows us to keep clean, maintainable, simple, readable code.