The RTK Query part of the Redux Essentials tutorial is phenomenal, but since it’s part of a much larger suite of documentation, I feel like the gem that is RTK Query is getting lost.
Many people think of Redux as a state management library, which it is. To them, the main value of Redux is that it makes it possible to access (and change) the application state from anywhere in the application. This misses the point of using something like Redux, so let’s zoom out a bit and take another look.
We’ve all seen this diagram or something like it:
You can see that, in this model, the UI has a place to display the current amount from the store and buttons that the user can click to indicate she wants to deposit or withdraw money. As soon as one of those buttons is clicked, we move outside of the component to dispatch the action based on what the user has requested. Note that at this point, the state has not changed. Next, the action arrives at the store, and the reducer looks at it to decide what, if anything, to do about the user’s request. If the reducer decides to honor the user's request, that change in state will then be propagated to the UI.
What do we gain from this? Well, first, let’s imagine the user has $5 in her account and clicks the “Withdraw $10” button. The reducer could decide to simply ignore that request and not go into a negative balance. Or, the reducer could route an error to a piece of state that’s monitored by a toast component. If we tried to put this logic into the UI component, it would make that component more complex, and it would mean changes that don’t really relate to that component would have to be made in that component.
At the same time, if we need that logic in another component, then we have a problem. Yes, people say that hooks solve this problem, but because ordinary hooks are so intertwined with the View logic, there usually is quite a lot of logic in the View to interact with the logic in a hook. So in practice you usually can’t just use a hook somewhere else without a lot of repeated logic in the components.
And the more error states and other situations we need to handle, the worse the complexity in your components will get. Not only that, if all that logic lives in your components, that implies that a lot of your state is just in a component. So as soon as the user navigates somewhere else, that state is lost.
By having a dedicated place for state to live and adding messaging in between the various pieces of the system, we create flexibility and resiliency in the system that is true to React’s reactive roots.
All of this starts to get much more complicated when you start to do API calls, because the original flavor of Redux, much like React, did not have a natural home for asynchronous logic. Luckily, Redux provided a mechanism for adding other middleware on top of it for responding to actions in more ways than just sending it through all the reducers and coming out with a new state.
The two most popular middlewares for dealing with stuff like this were redux-sagas and redux-thunk. For the record, I vastly preferred sagas, and I started to take Redux Toolkit seriously when I was looking for what people who liked sagas are doing now and found the listener middleware that had recently been added. Long story short, I found most of what I’d used sagas for was handled more ergonomically by RTK query. But that’s getting ahead of ourselves.
At a super high level, these middlewares let you specify a series of steps to take in response to a single dispatched action, including dispatching other actions, which would change the state while the steps were still going. That might look something like this:
It looks fairly simple in a diagram, but the logic to do this could be pretty verbose, and it was often difficult to test. And this code was usually repeated for every API call. Not only that, once you did all that, you still had to write a set of selectors to get the loading state, the error state, and the data for each API call. And that’s just for a relatively simple app.
A relatively common pattern is master/detail, where you get just enough data on a collection of “things” to show a list of them, and then users get more data by drilling in.
A lot of people think of Redux as a state management library, which it is. To them, the main value of Redux is that it makes it possible to access (and change) the application state from anywhere in the application. To me, this misses the point of using something like Redux, so let’s zoom out a bit and take another look.
We’ve all seen this diagram, or something like it:
You can see that, in this model, the UI has a place to display the current amount from the store, and buttons that the user can click to indicate she wants to deposit or withdraw money. As soon as one of those buttons is clicked, we move outside of the component to dispatch the action based on what the user has requested. Note that at this point, the state has not changed. Next, the action arrives at the store, and the reducer looks at it to decide what, if anything, to do about the user’s request. If the reducer decides to honor the user's request, that change in state will then be propagated to the UI.
What do we gain from this? Well, first, let’s imagine the user has $5 in her account and clicks the “Withdraw $10” button. The reducer could decide to simply ignore that request and not go into a negative balance. Or, the reducer could route an error to a piece of state that’s monitored by a toast component. If we tried to put this logic into the UI component, it would make that component more complex, and it would mean changes that don’t really relate to that component would have to be made in that component.
At the same time, if we need that logic in another component, then we have a problem. Yes, people say that hooks solve this problem, but because ordinary hooks are so intertwined with the View logic, there usually is quite a lot of logic in the View to interact with the logic in a hook. So in practice you usually can’t just use a hook somewhere else without a lot of repeated logic in the components.
And the more error states and other situations we need to handle, the worse the complexity in your components will get. Not only that, if all that logic lives in your components, that implies that a lot of your state is just in a component. So as soon as the user navigates somewhere else, that state is lost.
By having a dedicated place for state to live and adding messaging in between the various pieces of the system, we create flexibility and resiliency in the system that is true to React’s reactive roots.
All of this starts to get much more complicated when you start to do API calls, because the original flavor of Redux, much like React, did not have a natural home for asynchronous logic. Luckily, Redux provided a mechanism for adding other middleware on top of it for responding to actions in more ways than just sending it through all the reducers and coming out with a new state.
The two most popular middlewares for dealing with stuff like this were redux-sagas and redux-thunk. For the record, I vastly preferred sagas, and I started to take Redux Toolkit seriously when I was looking for what people who liked sagas are doing now and found the listener middleware that had recently been added. Long story short, I found most of what I’d used sagas for was handled more ergonomically by RTK query. But that’s getting ahead of ourselves.
At a super high level, these middlewares let you specify a series of steps to take in response to a single dispatched action, including dispatching other actions, which would change the state while the steps were still going. That might look something like this:
It looks fairly simple in a diagram, but the logic to do this could be pretty verbose, and it was often difficult to test. And this code was usually repeated for every API call. Not only that, once you did all that, you still had to write a set of selectors to get the loading state, the error state, and the data for each API call. And that’s just for a relatively simple app.
A relatively common pattern is master/detail, where you get just enough data on a collection of “things” to show a list of them, and then users get more data by drilling in.
Users can click in and out of the same detail entries, so an obvious way to improve performance is to cache the result of each detail call, then check to see if we already have it before we get it again. But this, again, adds complexity as we have to figure out how to store that, how to check for it, and then only run the api call if we don’t have it already. Running the call will be asynchronous, while simply returning a result we already have could be done synchronously.
When you start editing the data, it gets even more complicated. Suddenly, when you edit a record, you need to update the master list or somehow trigger the list to reload itself.
Before you know it, you can wind up with snarls of code trying to figure out what to do when and then how to get or update the data at the right time. If you have multiple people on a team, this can get even worse, because each one may come up with a slightly different way of handling each of these problems.
Redux Toolkit Query, or RTK Query, as the name implies, is built on top of Redux Toolkit, which I’m not going to cover in detail. In RTK Query, your API logic lives in a special slice that’s just for your API endpoints. There are two types of endpoints: queries and mutations. Queries only return one or more records, whereas mutations create, edit, or update the data in some way, possibly returning an updated state.
Within that slice, RTK query creates a “hash”, not just for each endpoint, but for each endpoint and the arguments it was called with. This makes it cheap and easy to keep the result of that query you just ran around for a little while in case the user clicks back on that detail.
And the state contained in each piece of the hash contains the pieces we previously had to write a lot of logic to manage:
The state that we were previously handling through thunks can now be obtained effortlessly with RTK query. This means we don't have to write any extra code to achieve the same functionality. While I'll discuss the code that we do have to write in a separate article, I believe it's easier for people to adopt a new method when they understand its benefits.