Understanding Redux Toolkit

Writing Redux logic by hand can be tedious and error-prone. Luckily for us, the Redux team created Redux Toolkit, or RTK, a library meant that's meant to address many of the pains experienced with plain old Redux. Redux Toolkit helps in the following ways:

  • developers can write redux logic more easily and quicker
  • writing immutable JavaScript logic for reducers is no longer required
  • we don't have to write action types and action creators for ourselves
  • no more installing multiple packages - Redux Toolkit as the name implies comes with many of the commonly used tools required when creating a redux app

As such, @reduxjs/toolkit has a noticeable reduction in code boilerplate, ensures best practices are followed, and makes the developer experience much better, once you get the hang of it. So let's!

How To Get Started

Prerequisites:

  1. A Package Manager
  2. Knowledge of JSX and ES5/6
  3. Redux DevTools Chrome Extension

To get started with RTK, run npm install @reduxjs/toolkit, and that's all you need!

Folder Structure

There are varied opinions about organizing code in a redux application. The Redux team recommends organizing your code by features, i.e. putting all the files related to a single feature in the same folder. Additionally, the redux logic is written in one single file in what is commonly known as the ducks pattern.

State Slice

In Redux, we store the state as an object tree, which is just a JS Object. For instance, if we have an application that has two features, posts and users, the global state would look like this:

{
  posts: [{...}, {...}, {...}],
  users: [{...}, {...} ,{...}]
}

A slice is the collection of all the redux logic for each particular feature. Above, we have two state slices.

createSlice

createSlice is one of the most fundamental functions of @reduxjs/toolkit. It accepts one object as an argument. The object accepts the following options:

  1. name:  this will be the name of your feature. The name will be used as the first part of the autogenerated action type strings eg {type: 'posts/addPost'}
    1. initialState for the reducer to work with
    2. reducers: this is an object where we define our synchronous reducer logic.
    3. extraReducers
import { createSlice } from "@reduxjs/toolkit";

const postsSlice = createSlice({
  name: "posts",
  initialState: [],
  initialState,
  reducers: {}
});

Immer

You might recall that Redux requires that all state updates be done immutably. To do this, we use ES6 features such as spread operators and other methods to return updated copies of the state object. However, the createSlice function from @reduxjs/toolkit uses a library called Immerjs, which allows us to mutate state directly.

So while before our logic for adding a post was:

switch(type){
...
 case "posts/addPost":
      return [...state, action.payload];
...
}

Now we can safely mutate our state directly like this:

 addPost: (state, action) => {
      state.push(action.payload);
    }

It's important to note that direct mutations are only possible using createSlice.

Reducers

The reducers key is an object, where we write our reducer functions for each case.

...
const postsSlice = createSlice({
  ...
  reducers: {
    addPost: (state, action) => {
      state.posts.push(action.payload);
    },
    updatePost: (state, action) => {},
    deletePost: (state, action) => {}
  }
});

export const { addPost } = postsSlice.actions;
export default postsSlice.reducer;

For every reducer function written inside the reducers object, createSlice will generate corresponding action types and action creators. We then export the reducer function and action creators following theducks pattern, which dictates that:

  1. the reducer function must be exported as the default
  2. the action creators must be exported as named exports

createSlice returns an object that includes the complete reducer function for the feature, an actions object containing all the auto-generated action creators, case reducers for the generated action types, and a getInitialState function that does what the name implies.

{
        name: "posts",
        reducer: (state, action) => newState,
        actions: {
          addPost: (payload) => ({type: "posts/addPost",  payload })
          updatePost: (payload) => ({type: "posts/updatePost",  payload })
        }
        caseReducers: {}
        getInitialState: () => state
      };

Each function defined in the reducers object is equivalent to one case logic in a switch statement. The actions object contains autogenerated action creators. These action creators return an action object, whose type value contains a prefix of the name we provided in the createSlice function, i.e. posts., while the payload is the data we will provide. For instance, adding a post would generate an action creator as follows:

dispatch(addPost({ author: 'Jane', message: 'howdy' })

console.log(addPost)  
// =>  {type: "posts/addPost",  payload: { author: 'Jane', message: 'howdy' }}

Every time addPost is dispatched from any component, postsSlice will call postsSlice.reducer() which will check for a matching case reducer. If found, it will run the matching case reducer function and return the new state, if not found, it will return the unchanged current state.

configureStore

Now that we have a reducer, we can initiate our store. RTK's configureStore function makes creating the store considerably easier compared to the plain redux way where you had to configure everything from scratch.

import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "../features/posts/postsSlice";

const store = configureStore({
  reducer: {
    posts: postsReducer
  }
});

export default store;

configureStore accepts a single configuration object parameter. In the object, we pass a reducer property which should be one of a reducer function to be used as the root reducer, or an object containing multiple slice reducers which will then be passed to combineReducers. Either way, it will end up returning one root reducer function.

Additionally, configureStore also automatically sets up the necessary middleware e.g. thunk middleware for asynchronous redux logic, and it also handles redux devtools integration into our app.

Now all that's remaining is to connect the store with the react app

import { createRoot } from "react-dom/client";

import App from "./App";
import { Provider } from "react-redux";
import store from "./app/store";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

and that's it!

Conclusion

In this article, we wrote redux logic using Redux Toolkit(RTK). We used RTK's createSlice function to write redux functions and generate actions. We then used RTK's configureStore to set up our store. Here's the final source code