What You'll Learn
- How to convert conventional data-fetching logic implemented with Redux Toolkit
to useRedux Toolkit Query`
The most common use case for side effects in Redux apps is fetching data. Redux apps typically use a tool like thunks, sagas, or observables to make an AJAX request, and dispatch actions based on the results of the request. Reducers then listen for those actions to manage loading state and cache the fetched data.
RTK Query is purpose-built to solve the use case of data fetching. While it can't replace all of the situations where you'd use thunks or other side effects approaches, using RTK Query should eliminate the need for most of that hand-written side effects logic.
RTK Query is expected to cover a lot of overlapping behaviour that users may have previously used
createAsyncThunk for, including caching purposes, and request lifecycle management (e.g.
In order to migrate data-fetching features from existing Redux tools to RTK Query, the appropriate endpoints should be added to an RTK Query API slice, and the previous feature code deleted. This generally will not include much common code kept between the two, as the tools work differently and one will replace the other.
If you're looking to get started with RTK Query from scratch, you may also wish to see
RTK Query Quick Start.
A common method used to implement simple, cached, data-fetching logic with Redux is to set up a slice using
createSlice, with state containing the associated
status for a query, using
createAsyncThunk to handle the asynchronous request lifecycles. Below we will explore an example of such an implementation, and how we can later go about migrating that code to use RTK Query instead.
RTK Query also provides many more features than what is created with the thunk example shown below. The example is only intended to demonstrate how the particular implementation could be replaced with RTK Query.
For our example, the design specifications required for the tool are as follows:
- Provide a hook to fetch data for a
pokemonusing the api: https://pokeapi.co/api/v2/pokemon/bulbasaur, where bulbasaur can be any pokemon name
- A request for any given name should only be sent if it hasn't already done so for the session
- The hook should provide us with the current status of the request for the supplied pokemon name; whether it is in an 'uninitialized', 'pending', 'fulfilled', or 'rejected' state
- The hook should provide us with the current data for the supplied pokemon name
With the above specifications in mind, lets first look at an overview of how this could be implemented traditionally using
createAsyncThunk combined with
The three snippets below make up our slice file. This file is concerned with managing our asynchronous request lifecycles, as well as storing our data & request statuses for a given pokemon name.
Below we create a thunk action creator using
createAsyncThunk in order to manage asynchronous request lifecycles. This will be accessible within components & hooks to be dispatched, in order to fire off a request for some pokemon data.
createAsyncThunk itself will handle dispatching lifecycle methods for our request:
rejected, which we will handle within our slice.
Below we have our
slice created with
createSlice. We have our reducers containing our request handling logic defined here, storing the appropriate 'status' and 'data' in our state based on the name we search with.
Below we have our selectors defined, allowing us to later access the appropriate status & data for any given pokemon name.
store for our app, we include the corresponding reducer from our slice under the
pokemon branch in our state tree. This lets our store handle the appropriate actions for our requests we will dispatch when running the app, using the logic defined previously.
In order to have the store accessible within our app, we will wrap our
App component with a
Provider component from
Below we create a hook to manage sending our request at the appropriate time, as well as obtaining the appropriate data & status from the store.
useSelector are used from
react-redux in order to communicate with the Redux store. At the end of our hook, we return the information in a neat, packaged object to be accessed in components.
Our code above meets all of the design specifications, so let's use it! Below we can see how the hook can be called in a component, and return the relevant data & status booleans.
Our implementation below provides the following behaviour in the component:
- When our component is mounted, if a request for the provided pokemon name has not already been sent for the session, send the request off
- The hook always provides the latest received
datawhen available, as well as the request status booleans
isRejectedin order to determine the current UI at any given moment as a function of our state.
A runnable example of the above code can be seen below:
Our implementation above does work perfectly fine for the requirements specified, however, extending the code to include further endpoints could involve a lot of repetition. It also has some certain limitations that may not be immediately obvious. For example, multiple components rendering simultaneously calling our hook would each send off a request for bulbasaur at the same time!
Below we will walk through how a lot of the boilerplate can be avoided by migrating the above code to use RTK Query instead. RTK Query will also handle many other situations for us, including de-duping requests on a more granular level to prevent sending unnecessary duplicate requests like that brought up above.
Our code below is for our API slice definition. This acts as our network API interface layer, and is created using
createApi. This file will contain our endpoint definition, and
createApi will provide us with an auto-generated hook which manages firing our request only when necessary, as well as providing us with request status lifecycle booleans.
This will completely cover our logic implemented above for the entire slice file, including the thunk, slice definition, selectors, and our custom hook!
Now that we have our API definition created, we need to hook it up to our store. In order to do that, we will need to use the
middleware properties from our created
api. This will allow the store to process the internal actions that the generated hook uses, allows the generated API logic to find the state correctly, and adds the logic for managing caching, invalidation, subscriptions, polling, and more.
At this basic level, the usage of the auto-generated hook is identical to our custom hook! All we need to do is change our import path and we're good to go!
As mentioned previously, our
api definition has replaced all of the logic that we implemented previously using
createSlice, and our custom hook definition.
Given that we're no longer using that slice any longer, we can remove the import and reducer from our store:
We can also remove the entire slice and hook files completely!
We've now re-implemented the full set of design specifications (and more!) in less than 20 lines of code, with room to easily expand by adding additional endpoints onto our api definition.
A runnable example of our re-factored implementation using RTK Query can be seen below: