Differences Between Redux and Redux Toolkit and Why Should You Upgrade
Saturday, November 20, 2021 Posted in Web-DevelopmentTags : React Redux Frontend
In this post, we will go over how Redux Toolkit has simplified and modernized Redux development with minimal boilerplate code and quality of life improvements
What is Redux Toolkit anyway ?
Redux Toolkit, which I’ll call RTK going forward, is the new and improved official, batteries included, opioninated package for writing standard Redux today as recommended by the Redux Developers team.
RTK is a Hooks based incarnation of Redux that simplifies Redux development, with quality of life improvements, like the ability to write mutable state updates in Reducers which are converted to Immutable state updates behind the scenes using a library called Immer, and auto-generated Actions from the Reducers themselves using Autodux, along with out-of-the-box support for Thunks.
There is also something called RTK query that can replace Async Thunks for data fetching in some cases, but we will not be getting into that for this article.
And like the Fat Homer vs Buff Homer banner image for this article, RTK has also slimmed down with almost no boilerplate needed to use it.
Installing RTK in an existing project :-
npm install @reduxjs/toolkit
Creating a new React CRA based Project with RTK pre-installed :-
npx create-react-app my-app --template redux
Differences and Improvements that RTK brings over old Redux
I will be using my project mern-login-signup-component
to illustrate the changes and improvements that RTK brings, this project was originally written in class based React with the old Redux implmentation, but in the past few days I have refactored it to functional component and hooks based React and RTK.
We will walk step-by-step through each Redux component, and show how RTK’s monumental improvements over the old way of writing Redux code, with the old Redux implementation going first, you will notice that boiler plate has been largerly removed, and writing code for say state changes in the Reducer has been simplified.
You can check out the MERN Login Signup Component on GitHub, as of writing this post I’m upgrading the project, so the code in the master repo may change, however the old redux code from the old version of this project can be accessed on it’s archive branch, and the new upgraded branch with the RTK code is in the modern-refactor-upgrade branch, with the RTK code written in JS that is shown in this post is based off, I will be refactoring the code to TypeScript soon, so the last commit with the RTK code in plain JS can be found on the commit d448afe86a.
Now since that’s out of the way, we will look at the RTK recommended file structure.
File Structure
Part of being an “opinionated” package, RTK if used with CRA to create a react project and even in the docs has a file structure that it recommends, however it is not necessary to follow this, it’s just a guideline, before RTK, actions would live in a folder called actions
, reducers would live in a folder called reducers
and components would have their own folder, while store.js
would live in the root src
directory
.
├── App.css
├── App.js
├── App.test.js
├── actions/
│ ├── authActions.js
│ ├── statusActions.js
│ ├── types.js
│ └── uiActions.js
├── components/
│ ├── HomePage.js
│ ├── Login.js
│ ├── Profile.js
│ ├── Register.js
│ └── style.css
├── index.js
├── reducers/
│ ├── authReducer.js
│ ├── index.js
│ ├── statusReducer.js
│ └── uiReducer.js
├── serviceWorker.js
└── store.js
The new Filestructure promoted by RTK follows a “feature” or domain based model, where every feature has it’s own folder, and that folder contains the components, the “slice” file that will contain it’s reducers and async thunks, and an API file that contains the API declarations, this can be seen in features/counter
which comes as a part of the CRA project with the redux toolkit template, which i’ve left as is, this contains the Counter.js
component file, the counterSlice.js
reducer and thunks file( from which actions are also auto generated ), and the counterAPI.js
file which contains the API.
.
├── App.css
├── App.js
├── App.test.js
├── app/
│ └── store.js
├── features/
│ ├── auth/
│ │ ├── Auth.css
│ │ ├── Auth.js
│ │ ├── LoadingSpinner.js
│ │ ├── Login.js
│ │ ├── Profile.js
│ │ ├── Register.js
│ │ ├── TimedError.js
│ │ ├── authService.js
│ │ └── authSlice.js
│ └── counter/
│ ├── Counter.js
│ ├── Counter.module.css
│ ├── counterAPI.js
│ ├── counterSlice.js
│ └── counterSlice.spec.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js
For my auth
feature’s file structure however, I have made a little change, the Thunks are declared in authServices.js
instead of authSlice.js
and I have no seperate authAPI.js
file, since the API is called as a part of the thunks in authServices.js
, because with a large amount of thunks/action creators/API calls, your Slice file would get crowded very soon, and If you find your components crowding your folder, you could shift them into a components
folder, it’s your project after all, lets move on to the Redux Store now
Redux Store file
Below you can see the classic Redux store.js
boilerplate, something you need to do for every Redux project and something you would just have to copy-paste like a cargo cult programmer just so that you can get to actually working on your SPA, you also have to import the root reducer declared elsewhere.
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [thunk];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, initialState, composeEnhancers(
applyMiddleware(...middleware),
));
export default store;
And now for the fully slimmed down, Size 0 version of the same store.js
file, written now using RTK.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import authReducer from '../features/auth/authSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
counter: counterReducer,
},
});
All that arcane boilerplate, reduced to one simple function, configureStore()
, which creates the store for you and has out of the box Redux DevTools support( this is a chrome extension that allows you to monitor Redux actions being fired and state changes), along with also doubling up as the root reducer function, where you import the reducers from your features and add them to the global state.
Using CRA’s RTK template, this is setup out of the box, you don’t need to do anything.
Action Creators, API Calls, Async Thunks
This section will deal with the action creators i.e you dispatch an API call, and the data returned is then dispatched as a state update, in an action object, we will be specifically looking at authActions.js
implemented according to the old Redux way of doing things.
import axios from "axios";
import { returnStatus } from "./statusActions";
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
REGISTER_SUCCESS,
REGISTER_FAIL,
AUTH_SUCCESS,
AUTH_FAIL,
LOGOUT_SUCCESS,
IS_LOADING,
} from "./types";
export const isAuth = () => (dispatch) => {
axios
.get("/api/users/authchecker",{withCredentials:true})
.then((res) =>
dispatch({
type: AUTH_SUCCESS,
payload: res.data
})
)
.catch((err) => {
dispatch({
type: AUTH_FAIL
});
});
}
//Register New User
export const register = ({ name, email, password }) => (dispatch) => {
// Headers
const headers = {
headers: {
"Content-Type": "application/json"
}
};
// Request body
const body = JSON.stringify({ name, email, password });
axios
.post("/api/users/register", body, headers)
.then((res) =>{
dispatch(returnStatus(res.data, res.status, 'REGISTER_SUCCESS'));
dispatch({ type: IS_LOADING })
})
.catch((err) => {
dispatch(returnStatus(err.response.data, err.response.status, 'REGISTER_FAIL'))
dispatch({
type: REGISTER_FAIL
});
dispatch({ type: IS_LOADING })
});
};
//Login User
export const login = ({ email, password }) => (dispatch) => {
// Headers
const headers = {
headers: {
"Content-Type": "application/json"
}
};
// Request body
const body = JSON.stringify({ email, password });
axios
.post("/api/users/login", body, headers)
.then((res) => {
console.log(res);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
dispatch({ type: IS_LOADING });
}
)
.catch((err) => {
dispatch(returnStatus(err.response.data, err.response.status, 'LOGIN_FAIL'))
dispatch({
type: LOGIN_FAIL
});
dispatch({ type: IS_LOADING })
});
};
//Logout User and Destroy session
export const logout = () => (dispatch) => {
axios
.delete("/api/users/logout", { withCredentials: true })
.then((res) =>
dispatch({
type: LOGOUT_SUCCESS,
})
)
.catch((err) => {
console.log(err);
});
}
As you can see, we have to import some pre-declared action types in ALL CAPS, and then manually dispatch actions if our API call is loading, it resolved successfully with some data or it failed with an error, further more, for error handling a seperate set of action creators and reducers is needed.
Now how it’s done in RTK.
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const headers = {
"Content-Type": "application/json",
};
export const isAuth = createAsyncThunk("auth/isAuth", async () => {
const response = await axios.get("/api/users/authchecker", {
withCredentials: true,
});
return response.data;
});
export const Login = createAsyncThunk(
"auth/Login",
async (data, { rejectWithValue }) => {
try {
const response = await axios.post("/api/users/login", data, headers, {
withCredentials: true,
});
return response.data;
} catch (err) {
// Custom error message from Server returned to reducer using rejectWithValue
// This error is available in action.payload unlike action.error for unhandled errors
return rejectWithValue(err.response.data);
}
}
);
export const RegisterThunk = createAsyncThunk(
"auth/Register",
async (data, { rejectWithValue }) => {
try {
const response = await axios.post("/api/users/register", data, headers, {
withCredentials: true,
});
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
export const LogoutThunk = createAsyncThunk("auth/Logout", async () => {
const response = await axios.delete("/api/users/logout", {
withCredentials: true,
});
return response.data;
});
RTK provides us with the createAsyncThunk function, this function allows us to do state changes in the reducer without explicitly having to dispatch loading or error actions, If we do want to dispatch a custom error message to the reducer, we can do this by wrapping our async API call in a try
catch
block, with the catch
block returning rejectWithValue()
with the error message as it’s parameter.
Furthermore, the action types don’t have to be declared as constants, and are now of the format of feature/actionName
as in auth/Login
for the login action or auth/Register
for the register actions, these Thunks can now be imported into the reducer.
Reducers in Old Redux compared with Reducers in an RTK Slice file
With the release of RTK, reducers seem to have got the most changes and improvements, we will see how below, with the old redux versions of authReducer.js
and statusReducer.js
, the authReducer handles our core auth actions, and the statusReducer handles errors.
import {
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT_SUCCESS,
REGISTER_SUCCESS,
REGISTER_FAIL,
AUTH_SUCCESS,
AUTH_FAIL
} from "../actions/types";
const initialState = {
isAuthenticated: null,
user: null,
};
export default function (state = initialState, action) {
switch (action.type) {
case REGISTER_SUCCESS:
return {
...state,
user: action.payload
};
case LOGIN_SUCCESS:
case AUTH_SUCCESS:
return {
...state,
isAuthenticated: true,
user: action.payload
};
case AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT_SUCCESS:
case REGISTER_FAIL:
case AUTH_FAIL:
return {
...state,
user: null,
isAuthenticated: false,
}
default:
return state;
}
}
import { GET_STATUS, CLEAR_STATUS} from '../actions/types';
const initialState = {
statusMsg: {},
respCode: null,
id: null
}
export default function(state = initialState, action) {
switch(action.type) {
case GET_STATUS:
return {
statusMsg: action.payload.msg,
respCode: action.payload.status,
id: action.payload.id
}
case CLEAR_STATUS:
return {
statusMsg: {},
respCode: null,
id: null
};
default:
return state;
}
}
import {
IS_LOADING,
} from "./../actions/types";
const initialState = {
loading: false
};
export default function (state = initialState, action ) {
switch (action.type) {
case IS_LOADING:
return {
...state,
loading: !state.loading
};
default:
return state;
}
}
As you can see, A Reducer in old Redux is an anonymous function that contains a switch case statement that returns a state update according to the action type it receives, these state updates are written to comply with Redux’s immutability of state rule, hence the use of the spread ...
operator to return a new object with copies of the previous state items and updated properties from action payload, or depending on the action type, we also have to write manual reducer state update logic for error messages and loading statuses.
We will now look on how RTK implements the Reducer and how RTK makes it easier for the developer to write reducer logic.
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import { isAuth, Login, RegisterThunk, LogoutThunk } from './authService';
const initialState = {
isAuthenticated: false,
user: {},
isLoading: false,
error: { isError: false, errMsg: "" },
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error.isError = false;
state.error.errMsg = "";
},
},
extraReducers: (builder) => {
builder
.addMatcher(
isAnyOf(isAuth.pending, Login.pending, RegisterThunk.pending),
(state) => {
state.isLoading = true;
}
)
.addMatcher(
isAnyOf(isAuth.fulfilled, Login.fulfilled,RegisterThunk.fulfilled),
(state, action) => {
state.isLoading = false;
state.user = action.payload;
state.isAuthenticated = true;
}
)
.addMatcher(
isAnyOf(isAuth.rejected, Login.rejected,LogoutThunk.fulfilled, RegisterThunk.rejected),
(state, action) => {
state.isLoading = false;
state.isAuthenticated = false;
state.user = {};
state.error.isError = true;
state.error.errMsg = action.payload; // action.payload contains the result of rejectWithValue upon error
}
)
},
});
// Exporting auto generated actions from reducers
export const { clearError } = authSlice.actions;
// Exporting pieces of state to be used in components with useSelector Hook
export const selectAuth = (state) => state.auth;
export const selectError = (state) => state.auth.error;
// Reducer export
export default authSlice.reducer
There’s a lot to take in, so let’s start from the top, going through each code block, at first we import the createSlice()
and isAnyOf
functions from Redux Toolkit, then we import our Thunks that we have declared in authServices
, after this we declare our initialState
object.
Now onto the meat and potatoes of this, the authSlice
which is our reducer only under a different name and by a different implementation, that is the createSlice()
function, this takes a single object as it’s parameter, and that object has to contain the name
of your reducer, your initialState
and the reducers
, with the extraReducers
being an option.
The Reducer implementation here is the reducer
object, which contains functions. You can think of the reducer object as the switch case statement from old Redux, and the functions in this reducer object can be considered the ‘cases’ in the switch statement.
However, unlike in old Redux, you can write code that directly mutates the state, instead of having to return a copy of the current state via the spread operator ...
and whatever fields with which you want to update the state from the action.payload
, this is done via a library called Immer, which turns your mutable state changes in the reducer functions to immutable state changes that Redux accepts, behind the scenes.
This method of writing mutable state changes like you would normally makes it much easier to deal with nested state changes, which is terrible to do using the normal immutable state change via object/array copies. Another feature in RTK is that the action creators are auto generated via this syntax export const { clearError } = authSlice.actions
, here we export the only reducer function that we have declared, and it can be dispatched in the component in which we import it.
Handling Async Thunks and external Actions using Extra Reducers
The last property of the parameter object that goes into createSlice()
is extraReducers
, extraReducers allows us to respond to actions defined elsewhere and not in the reducer
section of this Slice, the most common of which is responding to Async Thunks, examples of which will be shown below, or responding to actions defined in other reducers.
Below is a snippet of extraReducers
responding to actions created by Async Thunks that we imported in the top of the file.
{
extraReducers: (builder) => {
builder
.addMatcher(
isAnyOf(isAuth.pending, Login.pending, RegisterThunk.pending),
(state) => {
state.isLoading = true;
}
)
.addMatcher(
isAnyOf(isAuth.fulfilled, Login.fulfilled,RegisterThunk.fulfilled),
(state, action) => {
state.isLoading = false;
state.user = action.payload;
state.isAuthenticated = true;
}
)
.addMatcher(
isAnyOf(isAuth.rejected, Login.rejected,LogoutThunk.fulfilled, RegisterThunk.rejected),
(state, action) => {
state.isLoading = false;
state.isAuthenticated = false;
state.user = {};
state.error.isError = true;
state.error.errMsg = action.payload; // action.payload contains the result of rejectWithValue upon error
}
)
},
}
So as we can see, the extraReducers property leads to a callback function which provides the builder object as it’s argument, and this object has 3 functions addCase()
, addMatcher()
and defaultCase()
, I have used addMatcher()
here because many of the actions from the Async Thunks have the same state changes.
This addMatcher()
function takes two arguments, a matcher function which according to the RTK docs should be a “type predicate” function, but what this means is that the function needs to return true for whatever type it receives as an argument, the second function is a reducer function as in previous examples, where you can change state based on the action payload.
I am using isAnyOf()
, which is a matcher function provided by RTK, this function is basically like an || ( Logical OR ) operator, returning true if even one of the arguments matches, for e.g in this code snippet, if the action type is either isAuth.fulfilled
, Login.fulfilled
or Register.fulfilled
, isAnyOf()
returns true, and the reducer function is callled, and the values in the state are changed or set accordingly
.addMatcher(
isAnyOf(isAuth.fulfilled, Login.fulfilled,RegisterThunk.fulfilled),
(state, action) => {
state.isLoading = false;
state.user = action.payload;
state.isAuthenticated = true;
}
)
An Async Thunk has 3 states( or “action creators” to use the official RTK docs term ) pending
, fulfilled
and rejected
, as above, each of there 3 can be handled independently in extraReducers, pending
just means that the Async Thunk promise hasn’t been resolved
/ successful or rejected
/ failed, so this is where you can plug in an isLoading
state value that is a boolen, if the Async Thunk has been resolved
, you can do your typical state updations using the data from action.payload
and finally if your Async Thunk is rejected
i.e fails with an error, you can access the error directly using action.error
, or if an error message has been provided via rejectWithValue
in the Async Thunk, this error message can be accessed using action.payload
.
More on Extra Reducers
In the above section, the code snippets were using addMatcher()
, however Async Thunks that don’t need to do the same state changes, you can use the more simpler addCase()
function, which is recommended by the RTK docs since it plays well with TypeScript, The below code snippet is taken from the counterSlice.js
file
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
Another way to write the above would be using the “Map Object” notation, It however only works in JS, not in TypeScript and has less integration with IDEs.
{
extraReducers: {
[incrementAsync.pending] : (state) => {
state.status = 'loading';
},
[incrementAsync.fulfilled] : (state, action) => {
state.status = 'idle';
state.value += action.payload;
},
},
}
You can check the official RTK docs on createSlice and createReducer for more details.
Slice File Exports
We are now at the end of the Slice file, where we export our generated actions from the reducer object, our selectors containing specific pieces of state, and finally the reducer itself to be added to the store.js
file, The below code snippet illustrates this.
export const { clearError } = authSlice.actions;
// Exporting pieces of state to be used in components with useSelector Hook
export const selectAuth = (state) => state.auth;
export const selectError = (state) => state.auth.error;
// Reducer export
export default authSlice.reducer
In this snippet, clearError
is the reducer function we declared in the reducer
object, and because of the Autodux library, and action creator is automatically generated with the same name, which we desctructure from authSlice.actions
and export.
We next have our selectors, selectAuth
and selectError
, these are simple callback functions that return the specific piece of state we tell it to, for e.g selectAuth gets the entire auth state object ` {isAuthenticated: false, user: {}, isLoading: false, error: { isError: false, errMsg: “” }} while
selectError only exports the
error` property/object of the auth state.
Finally we export the reducer itself( i.e reducer and extraReducers combined ) from the authSlice via export default authSlice.reducer
.
Using the Redux State Store in Components
Accessing your Redux global state store in a component was always the most boilerplate-y part of old Redux, needing propType
declarations for every piece of state and action you needed to use, and MapStateToProps
and MapDispatchToProps
callbacks to be written and supplied to the connect()
function that would wrap arround your component, combined with the verbosity of React Class Components, lead to code like this :-
import React, { Component } from 'react'
import { connect } from "react-redux"; // API to connect component state to redux store
import PropTypes from "prop-types";
import { buttonClicked,isLoading } from "../actions/uiActions";
import { login } from "../actions/authActions";
import { Link } from 'react-router-dom'
import './style.css';
class Login extends Component {
state = {
email: "",
password: "",
msg: ""
}
static propTypes = {
buttonClicked: PropTypes.func.isRequired,
isLoading: PropTypes.func.isRequired,
button: PropTypes.bool,
login: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool,
status: PropTypes.object.isRequired,
loading: PropTypes.bool
};
componentDidMount() {
this.props.buttonClicked();
}
componentDidUpdate(prevProps) {
const status = this.props.status;
if (status !== prevProps.status) {
if (status.id === "LOGIN_FAIL") {
this.setState({ msg: status.statusMsg });
}
}
};
onChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
onSubmit = (e) => {
e.preventDefault();
const { email, password} = this.state;
const user = { email, password};
this.props.isLoading();
this.props.login(user);
};
render() {
let className = 'divStyle';
if (!this.props.button) {
className = 'formStyle';
}
return (
<div className={className}>
{/*Login component elements go here*/}
</div>
)
}
}
const mapStateToProps = (state) => ({
button: state.ui.button,
isAuthenticated: state.auth.isAuthenticated,
status: state.status,
loading: state.ui.loading
});
export default connect(mapStateToProps,{ login, isLoading, buttonClicked })(Login);
Redux Toolkit simplifies this immensely to just the usage of a simple useSelector
hook to import pieces of state into your component, and simple imports of actions and usage of the useDispatch
hook to dispatch these actions, to MapXToProps
, no PropType declarations and no connect()
HoC.
import React, { useEffect, useState } from 'react'
import './Auth.css';
import {Link} from 'react-router-dom';
import { useDispatch, useSelector } from "react-redux";
import {Login as LoginService} from './authService';
import authSlice, { selectError, clearError, selectAuth } from "./authSlice";
import TimedError from './TimedError';
import LoadingSpinner from './LoadingSpinner';
function Login({buttonClicked}) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const error = useSelector(selectError);
const auth = useSelector(selectAuth);
useEffect(() => {
buttonClicked(false);
dispatch(clearError());
},[])
const handleSubmit = () => {
const data = {'email':email, 'password':password};
dispatch(LoginService(data));
}
return (
{/*Login Component Elements go here*/}
)
}
export default Login
This results in a much more slimmed down component, with all the boiler plate cruft removed, I have also elided the Login form elements from these code snippets in order to make it easier to read, but you can get the whole code file on my GitHub repo.
You can also read more about advanced usage of useSelector
and useDispatch
on the RTX Docs on Hooks
Conclusion and Thoughts
Redux Toolkit is probably the most impactful upgrade of a software library that I use, massively improving usability, developer experience and removing pretty much most of the boilerplate.
RTK is also a part of the overall transition of React development away from Class Components to Functional Components and Hooks, something which even other prominent React libraries like React-Router have done with their recent v6 release, along with the general trend of moving towards TypeScript by the React Community( Redux Toolkit is built using TypeScript ).
I cannot comment on the time and ease of refactoring existing projects using old Redux to RTK, but going forward RTK is what i’m going to use for my new projects, whether professional or personal.
Thanks for reading this article, and I hope you enjoyed reading it and learned something about RTK. Cheers! 🍻