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:
npm install redux react-redux @reduxjs/toolkit
yarn add redux react-redux @reduxjs/toolkit
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:
// Action
export const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME';
// Action creator
export const updateListName = (name) => ({
type: UPDATE_LIST_NAME,
payload: { name }
});
// Reducer
const reducer = (state = 'My to-do list', action) => {
switch (action.type) {
case UPDATE_LIST_NAME: {
const { name } = action.payload;
return name;
}
default: {
return state;
}
}
};
export default reducer;
With createSlice
, it looks like this:
// src/redux-toolkit/state/reducers/list-name
import { createSlice } from '@reduxjs/toolkit';
const listNameSlice = createSlice({
name: 'listName',
initialState: 'My to-do list',
reducers: {
updateListName: (state, action) => {
const { name } = action.payload;
return name;
}
}
});
export const {
actions: { updateListName },
} = listNameSlice;
export default listNameSlice.reducer;
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:
{ ...tasks, [id]: { id, name, checked: false } };
With createSlice
, we can now directly mutate the state:
tasks[id] = { id, name, checked: false };
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:
import React from 'react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import reducer from '../reducers';
const store = configureStore({
reducer
});
export const TasksProvider = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
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:
import { combineReducers } from 'redux';
import listNameReducer from './list-name';
import tasksReducer from './tasks';
const reducer = combineReducers(listNameReducer, tasksReducer);
export default reducer;
With configureStore
, if you pass in multiple reducers it will do this combineReducer
step for you:
const store = configureStore({
reducer: {
listName: listNameReducer,
tasks: tasksReducer
}
});
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:
// src/redux-toolkit/components
const ReduxToolkitApp = () => (
<>
<h2>Redux Toolkit</h2>
<TasksProvider>
<Name />
<Tasks />
<CreateTask />
</TasksProvider>
</>
);
Create and use selectors with useSelector
:
// src/redux-toolkit/state/selectors
export const tasksSelector = (state) => state.tasks;
// src/redux-toolkit/components/tasks
const tasks = useSelector(tasksSelector);
And dispatch actions to modify the state with useDispatch
:
// src/redux-toolkit/components/name
const dispatch = useDispatch();
dispatch(updateListName({ name }));
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.
// src/redux-toolkit/state/selectors
export const tasksSelector = (state) => state.tasks;
// src/components/redux-toolkit/components/tasks
const Tasks = () => {
const tasks = useSelector(tasksSelector);
return <TasksView Task={Task} tasks={tasks} />;
};
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:
import { createSelector } from '@reduxjs/toolkit';
const completedTasksSelector = createSelector(
state => state.tasks,
tasks => tasks.filter(task => task.checked)
)
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:
// component code
const dispatch = useDispatch();
createTaskAPI(data).then((newTask) => {
dispatch(createTask(newTask));
});
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:
dispatch(createTask(data));
And then inside the action creator, we wait for the API to return before returning an action:
const createTask = async (data) => {
const newTask = await createTaskAPI(data);
return { type: 'createTask', payload: newTask };
}
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:
// component code
const dispatch = useDispatch();
createTask(dispatch, data);
// action creator
const createTask = async (dispatch, data) => {
const newTask = await createTaskAPI(data);
dispatch({ type: 'createTask', payload: newTask });
}
The problem with this approach is that your component code now has to know that for certain actions it does this:
action(dispatch, data);
But then for other actions it does this:
dispatch(action(data))
Option 3: Use redux-thunk
redux-thunk
simplifies this by giving us access to the dispatch
function inside the action creator:
// component code
const dispatch = useDispatch();
dispatch(createTask(data));
// action creator
const createTask = (data) => {
return async (dispatch) => {
const newTask = await createTaskAPI(data);
dispatch({ type: 'createTask', payload: newTask });
}
}
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!
Top comments (5)
Hi, I'm a Redux maintainer. Very nice post!
Note that Redux Toolkit is orthogonal to React-Redux, and RTK has no UI-related APIs at all. You can have hand-written Redux logic with React-Redux hooks, RTK with React-Redux
connect
, or any combination thereof. (In fact, since RTK is its own independent lib, you can even use it without a Redux store to generate reducers foruseReducer
, or by using Redux as the state management layer for another framework like Angular.)Redux Toolkit is a wrapper around the Redux core, both in terms of actual API usage (
configureStore
>createStore
), and conceptually. You still have to understand "dispatching actions", "reducers", "immutability", etc. Given that Redux has been around for several years, and there's millions of lines of Redux code out there already, we couldn't just shove these APIs into the Redux core.That said, we are officially recommending RTK as the default way to write Redux logic. Also, I'm currently working on a rewrite of the Redux core docs, and as part of that I'm working on a new "Quick Start" tutorial for the Redux docs that teaches Redux at a higher level ("do this", not "how/why it works"), and teaches RTK and the React-Redux hooks API as the default. It's not done yet, but I'm hoping to tackle the last section of the first draft shortly:
deploy-preview-3740--redux-docs.ne...
For some more background on how and why we came up with RTK and what the intent is, see:
Thanks for the comment! The new docs look good - will you be rewriting the basic/advanced tutorials into using the toolkit too?
I also really like that style guide / recommendations page you've linked - lots of useful tips in there.
The current "Basic/Advanced" tutorial sequence tries to teach Redux "from the bottom up", ie, here's all the concepts, here's how everything works, here's how to do everything "by hand". We still need to have that kind of a tutorial so that people understand what's actually going on inside.
My plan is to have both the "Quick Start" tutorial as the main way to get going, and an updated version of the "Basic/Advanced" sequence that teaches how it all works. The updated bottom-up sequence will remove outdated references and terms (like "If you've ever written a Flux app, you'll feel right at home"), show simpler patterns (inlined action types with a naming convention like
{type: "todos/todoAdded"}
instead ofconst ADD_TODO = "ADD_TODO"
, putting logic for a feature in a single file instead of splitting across multiple files), and probably redo the example apps. Then, at the end of that sequence, we'll point out that RTK does all this work for you already and you should use RTK instead of doing it all by hand.You can see my plans for reworking the docs overall in github.com/reduxjs/redux/issues/3592 .
And thanks! I did write the Style Guide page several months ago, and I've gotten a lot of very positive feedback that it's helpful.
(Also, this is taking me a really long time because right now I'm the only one putting any effort into this rewrite, and I'd really love to have some more folks helping me out! :) )
I'd love to help out somewhere - I've only had real-world experience with old Redux (no hooks or RTK) so my knowledge may be a little bit outdated though. Is there somewhere specific I can start helping?
Yeah, I'd appreciate it!
That "Overview" issue lists all the stuff I want to do for the docs, overall, and links to a prior issue where I laid out why I was wanting to make these changes.
Basically anything in that list is stuff I'd like help with. For example, the "Usage with Immutable.js" recipes page needs to go away, because we now specifically advise against using Immutable.js. There's a lot of pages that need to be shuffled around, FAQ entries ought to be updated to try to match the recommendations in the Style Guide, new "real world usage" sections need to be written, etc, etc.
Feel free to comment in github.com/reduxjs/redux/issues/3592 and we can figure out what would be a good place to start.