Getting started with state management using Redux Toolkit
Redux Toolkit is a library that provides helper functions to simplify how you use Redux. It was created in response to criticisms that Redux required a lot of boilerplate code and was confusing to get set up.
My previous post in this series explored how state management works with Redux. This post will explore how Redux Toolkit changes things with createSlice
, as well as looking into some of the additional features it provides like createSelector
and redux-thunk
.
If you want to follow along, I have created a repository for the example app created in this guide at react-state-comparison.
This post assumes knowledge of how to render components in React, as well as a general understanding of how hooks work. It also assumes you have a basic understanding of Redux, as we will be making some comparisons to it in this post.
Getting started
To get started with Redux Toolkit, you’ll need to install these libraries using your package manager of choice:
A brief overview of terms
There are a couple of terms that are important when managing state with React and Redux:
- A store is a central location where we store all the state for our app.
- An action is in charge of telling the reducer to modify the store. We dispatch these actions from the UI.
- We also have action creators which are functions that create actions for us
- The reducer handles doing what the action tells it to do (i.e. making the necessary modifications to the store).
Create reducers and actions with createSlice
The first improvement that Redux Toolkit seeks to make is to reduce the amount of code you need to create your actions and reducers.
With plain Redux, here is the code that we would use to modify the name of our to-do list:
With createSlice
, it looks like this:
With createSlice
, we only need to define our reducer and our actions will be created for us! It helps to simplify the amount of boilerplate code developers have to write.
You no longer have to worry about mutating state
With plain Redux, you have to be careful not to directly mutate the state, as it will cause unexpected behaviours. For instance, to add a new task to your store you would have to do something like this:
With createSlice
, we can now directly mutate the state:
Behind-the-scenes it’s using the immer library, so we’re not actually mutating the state.
I really love this feature, since it makes the reducer code easier to understand, and it also removes the responsibility from the developer to have to learn how to use libraries like immer.
Creating and initialising our store
After creating a reducer, you can set up your store using it. Here we’ll be using the configureStore
function from the toolkit to do so:
What differentiates configureStore
from Redux’s createStore
is that it provides a few extra defaults out of the box. This helps you get started quicker when setting up a new app. I’ll be touching on one of the features it provides (redux-thunk
) later in this post.
configureStore can combine reducers
Redux provides a combineReducers()
function to split your reducers into multiple files:
With configureStore
, if you pass in multiple reducers it will do this combineReducer
step for you:
Finishing it off
Beyond this point, our app is otherwise identical to how you would set up a plain Redux app using hooks. To summarise, we first need to wrap our app in the TasksProvider
that we created:
Create and use selectors with useSelector
:
And dispatch actions to modify the state with useDispatch
:
Now let’s jump into a couple of other features that React Toolkit provides.
createSelector
Redux Toolkit lets us create selectors using createSelector
. This is a pre-existing feature, and can already be used in any Redux app by adding the reselect library. Redux Toolkit has taken the step of including it by default.
When do we need createSelector?
With selectors, the component will only re-render when what the selector is returning has changed.
For instance, our Tasks
component will only re-render when state.tasks
has changed.
However, each time any part of the state changes, the tasksSelector
will re-run, and calculate what it needs to return. This could cause performance problems if we had a huge list of tasks, and our selector was doing some sort of calculation (like filtering on whether a task was done or not).
createSelector
lets you create a memoized selector. What this means is that it will cache the result of its calculation, and only re-calculate once things have changed. In our case, we could create a selector that only re-calculates once state.tasks
has changed:
To learn more about createSelector I’d recommend you to check out the readme on the reselect library.
Redux middleware and redux-thunk
The final thing we’ll be touching on in this post is Redux middleware. These are third-party libraries that you can add on to your Redux setup to add extra functionality. When using Redux Toolkit’s configureStore
API, we will get a couple installed out of the box. One of them is redux-thunk
.
When do we need redux-thunk?
A common use-case for redux-thunk
is when making API calls. Imagine we stored our newly created tasks in the backend. Our flow would look something like this:
- User types and then presses the “create task” button
- We call the create task endpoint
- We wait for the endpoint to return that the task has successfully been created
- We show the newly created task at the bottom of the list
There are a couple of ways we could tackle this scenario.
Option 1: Let the UI call the endpoint
We could let the UI call the task creation endpoint, and only when it successfully returns, we call our createTask
action:
However generally you want to keep your component code focused on rendering things, and move code that interacts with your APIs elsewhere.
So it would be nice if the UI could just do this:
And then inside the action creator, we wait for the API to return before returning an action:
However the dispatch
function is expecting an action - we can’t pass in an async function!
Option 2: Pass in a dispatch argument
The workaround for this is that we pass in the dispatch
function as an argument, and then wait to dispatch the action until the endpoint has returned:
The problem with this approach is that your component code now has to know that for certain actions it does this:
But then for other actions it does this:
Option 3: Use redux-thunk
redux-thunk
simplifies this by giving us access to the dispatch
function inside the action creator:
Now our component code no longer has to know whether it needs to pass in dispatch
as an argument or not, and can always just wrap actions in the dispatch
function!
Since this middleware is so useful, it has been included by default when you set up your store using configureStore()
.
Conclusion
Redux Toolkit is a great library for simplifying the Redux code you write. By default it also provides useful tools that many codebases will need, like createSelector
and redux-thunk
. If you’re a developer working on a new codebase, I’d recommend jumping straight into using this toolkit.
Unfortunately, for developers working on older and larger codebases, you could end up having to wrap your head around multiple ways of writing Redux:
- Old Redux with
connect()
- New Redux with hooks like
useSelector()
- New-new Redux with Redux Toolkit
I could see this getting confusing and hard to manage, especially for developers who are new to the codebase. It’s also interesting that the toolkit lives in its own separate library, instead of being part of the main react-redux library. I understand there may be some good reasons behind it, but I think it could confuse developers new to Redux into thinking it was optional, when I think it really should become compulsory and the new standard.
Nevertheless I think this is a great step forward in simplifying Redux, and I’m interested to see where things goes in the future.
Thanks for reading!