Efficient Redux Development with Redux Toolkit

November 20, 2023 - 6 min read - 0 views

I've been using Redux for managing and updating the global state in my React apps for a while. Redux simplifies state management and implements complex performance optimizations, ensuring your components only re-render when necessary. However, a major complaint about React-Redux is the boilerplate code. 🍽️

From configuring the store to integrating Redux DevTools and maintaining actions, dispatch, and reducers, it can be quite cumbersome.

To overcome these challenges, we have two options:

1. Stop using Redux! ⚠️

Yes, you might not need Redux at all. React's built-in Context API can be used to inject state data into different components. For simpler apps, libraries like react-query can be used for data caching along with Context for state management.

2. Use Redux Toolkit (RTK):

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.

Redux Toolkit was created to address three common concerns about Redux:

  • "Configuring a Redux store is too complicated."
  • "I have to add a lot of packages to get Redux to do anything useful."
  • "Redux requires too much boilerplate code."

It includes utilities to simplify common use cases like store setup, creating reducers, immutable update logic, and more. Additionally, it follows the DUCK 🦆 pattern, significantly reducing boilerplate code.

🧠

Redux addons like Redux DevTools Extension and Redux Thunk are built-in with RTK. 🤯🤯

Before diving into the code, you should have a basic understanding of Redux concepts like:

  • Store
  • Actions
  • Reducer
  • Provider Component

If you're not familiar, check out the latest basic tutorial on React-Redux docs. They offer fantastic tutorials for getting started with Redux.

Installation

Start with the official Redux+JS template for Create React App:

npx create-react-app my-app --template redux

Alternatively, you can add the following packages to an existing React application:

yarn add redux react-redux @reduxjs/toolkit

We'll visualize a basic todo app and demonstrate the Redux part of it.

configureStore()

This API wraps createStore to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add Redux middleware, include redux-thunk by default, and enable the Redux DevTools Extension out of the box.

import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "../slices/todoSlice";
 
export default configureStore({
  reducer: {
    todos: todoReducer,
  },
});

Here, we import configureStore() and pass the todoReducer into the root reducer, configuring it to the store. We access the values in state.todos.

createSlice()

createSlice() is a collection of Redux reducer logic and actions for a single feature in your app. It automatically creates the action type strings, so no need to create action types for reducers.

// todoSlice.js
import { createSlice } from "@reduxjs/toolkit";
 
export const todoSlice = createSlice({
  name: "todos",
  initialState: {
    todoList: [],
  },
  reducers: {
    addTodo: {},
  },
});
 
export const { addTodo } = todoSlice.actions;
 
export default todoSlice.reducer;

It takes three things:

  • Name: A string name for this slice of state. Generated action type constants will use this as a prefix.
  • Initial State: Sets the todoList to an empty array.
  • Reducers Object: Contains the reducer logic.

Finally, we export the actions and reducer.

AddTodo Reducer

This reducer takes the state and action, and pushes the payload into the state.

addTodo: {
  reducer: (state, action) => {
    state.todoList.push(action.payload);
  },
},

Previously, we avoided mutating the state because Redux state is immutable. But Redux Toolkit uses Immer under the hood, allowing us to directly mutate the state. Here, we push the payload data into the todoList.

useDispatch()

To call the data in our component, we use the useDispatch() hook.

// AddTodo Component
const dispatch = useDispatch();
import { addTodo } from "../../redux/slices/todoSlice";
 
const handleSubmit = () => {
  dispatch(addTodo(value));
};

Here, we use the useDispatch() hook to dispatch the addTodo action. Now, we have successfully added the Todo to the state. 🕺

Add other fields to the payload

To keep the list in sync with the state in React, we add a key to each list item using a prepare callback.

// todoSlice.js
import { createSlice, nanoid } from "@reduxjs/toolkit";
 
export const todoSlice = createSlice({
  name: "todos",
  initialState: {
    todoList: [],
  },
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.todoList.push(action.payload);
      },
      prepare(value) {
        return {
          payload: {
            key: nanoid(),
            value: value,
          },
        };
      },
    },
  },
});
 
export const { addTodo } = todoSlice.actions;
 
export default todoSlice.reducer;

We import nanoid() from Redux Toolkit and add the prepare callback, which assigns a unique key using nanoid() and the value as the todo value.

useSelector()

The useSelector() hook selects the state from the store and loads it into the component.

// TodoList component
import { useSelector } from "react-redux";
 
const todoListdata = useSelector((state) => state.todos.todoList);
 
//... map the todoListdata

Fetch Data from an API

Previously, we used middlewares like redux-thunk or redux-saga for fetching data. Redux Toolkit simplifies this with built-in redux-thunk and the createAsyncThunk function.

createAsyncThunk generates promise lifecycle action types based on the action type prefix and returns a thunk action creator that runs the promise callback and dispatches the lifecycle actions based on the returned promise.

It creates three types of actions automatically:

  • Pending ⏳
  • Fulfilled ✅
  • Rejected ❌
// todoSlice.js
import { createSlice, nanoid, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
 
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
  const { data } = await axios.get(
    "https://jsonplaceholder.typicode.com/todos/",
  );
  return data;
});

createAsyncThunk takes two parameters:

  • Action Type Name: 'todos/fetchTodos', which appends the three action types (pending, fulfilled, rejected).
  • Callback Function: Performs async actions and returns the todos data.

Adding Status and Error to Initial State

We add status and error to the initial state to track the action status and handle errors.

// todoSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
 
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/todos/",
  );
  return response.data.todoList;
});
 
export const todoSlice = createSlice({
  name: "todos",
  initialState: {
    todoList: [],
    status: "idle",
    error: null,
  },
  reducers: {},
  extraReducers: {
    [fetchTodos.pending]: (state) => {
      state.status = "loading";
    },
    [fetchTodos.fulfilled]: (state, action) => {
      state.status = "succeeded";
      state.todoList.push(...action.payload);
    },
    [fetchTodos.rejected]: (state, action) => {
      state.status = "failed";
      state.error = action.error.message;
    },
  },
});
 
export const { addTodo } = todoSlice.actions;
 
export default todoSlice.reducer;

createAsyncThunk creates its own actions, so we need extraReducers to handle those actions and update the state accordingly. It checks the status of the state and maps the status and error in the state. If the data is fetched successfully, it pushes the data into todoList.

Now, we can check the status and map the data in the corresponding component.

That's it! 👋

Redux Toolkit may seem confusing at first, but once you get the hang of it, it significantly reduces boilerplate in your application.