Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

Smarter Redux with Redux Toolkit

10 min read 2931

Smarter Redux With Redux Toolkit

Editor’s note: This article was updated on 22 April 2022 to include up-to-date information about Redux-Saga and Redux Toolkit.

When Redux first appeared on the frontend horizon, many expected it to be a solution to all our state management problems. Its association with React was so deep that people began to believe that React was somehow incomplete without Redux, though this idea was categorically rejected by Redux creator Dan Abramov.

Gradually, developers began to realize Redux’s limitations, which led to a chain of debate around whether there are better ways to manage global state — and, indeed, whether Redux is a viable solution to begin with.

Contents

What is Redux Toolkit?

Many of the arguments against Redux were born of a number of opinions and “best practices” that later came to be viewed as requirements. (In actuality, Redux in its bare form is quite simple and easy to understand.) But one of the most popular and enduring critiques is the amount of code Redux adds to the app, which many feel is unnecessary.

These debates led to the development of Redux Toolkit (RTK), “the official, opinionated, batteries-included toolset for efficient Redux development.” The Redux team has put a huge amount of effort into this and, without a doubt, has produced a remarkable result.

RTK resolves many of the arguments related to boilerplate and unnecessary code. As mentioned in its official docs, it helps to solve three major problems people had with Redux:

  1. “Configuring a Redux store is too complicated”
  2. “I have to add a lot of packages to get Redux to do anything useful”
  3. “Redux requires too much boilerplate code”

Here we will see how we can leverage RTK’s API to make our Redux applications smaller yet still powerful. We will use a React and an RTK template to bootstrap our application.

Our app will have the following architecture for containers:

  • Redux, for state management
  • React-Redux, for selecting state from the global store and dispatching actions
  • Immer, for handling immutability in stores

Later on, we will install Redux-Saga and see how it can be used for async tasks.

One important note: Redux Thunk is available with RTK as a default option for async tasks and is highly recommended for simple data fetching tasks. You can find its guide here. In this post, however, we will use Redux-Saga for better understanding of middleware integration with RTK. It would be well worth reading through this post if your unfamiliar with Redux-Saga.

You can find the source code for the application here.



Bootstrapping React with Redux Toolkit

One of Redux’s most discussed flaws is the effort required to integrate it into an existing application. With Redux Toolkit comes the option to bootstrap a React app with Redux Toolkit included.

To do so, run the following command in your terminal:

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

When the download is complete, open up the application in your text editor, and we will examine the key file that makes the Redux store function:

// store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
  // other options e.g middleware, go here
})

The Redux store — located in app/store.js — is the central station of our Redux Toolkit application. This file makes use of RTK’s configureStore API, which, according to the RTK docs, is a friendly abstraction over the standard Redux createStore function. It adds good defaults to the store setup for a better development experience.

configureStore accepts a single configurations object with multiple parameters, the most important being reducer.

reducer is an object that stores different slices in our application and reflects it in the Redux store, as seen through the Redux DevTools extension.

Redux-devtools-extension


More great articles from LogRocket:


In our template, a counterSlice is added to reducer object — more explanation on what slices are soon.

To make data from our store accessible to all components in our application, the Provider component from react-redux is used to wrap the entire application, taking in the Redux store as the value for the store prop:

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

Redux Toolkit and the ducks pattern

Though Redux is not dependent on how your files are structured, it plays an important role in the maintenance of large-scale applications. It’s better to group files based on features rather than file types.

The widely adopted ducks pattern suggests keeping all Redux functionality in a single file. This file will export a reducer function by default along with all actions, creators, and constants, if required. It also suggests the pattern for action types.

The name comes from the last part of Redux — dux — and has become a highly recommended pattern for Redux applications.

RTK follows the ducks pattern and combines reducers, actions, and constants in one file called a slice. Each slice will provide an initial state and a reducer function for an object in store:

// counterSlice.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';
const initialState = {
  value: 0,
  status: 'idle',
};

// asynchronous function with createAsyncThunk
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount) => {
    const response = await fetchCount(amount);
    return response.data;
  }
);

// Redux Toolkit slice
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
  // The `extraReducers` field lets the slice handle actions defined elsewhere,
  // including actions generated by createAsyncThunk or in other slices.
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// more code...
export default counterSlice.reducer;

The counterSlice file uses the createSlice method from RTK. This method returns an object with reducers and actions that can be used for injection with other middleware.

createSlice

Redux Toolkit can replace a series of actions/reducers/constants using its createSlice API. It is a helper method to generate a store slice. It takes the name of the slice, initial state, and a reducer function to return a reducer, action types, and action creators.

In return, it gives us the name of the slice, action creators, and reducer functions. All of these can be used for injection of the slice, dispatching actions, and other cases, depending upon implementation.

Looking at counterSlice.js, we can see that createSlice returns an object with a name and reducers that can be dispatched from anywhere in our application like so:

// Counter.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import {
  increment,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
  const dispatch = useDispatch();

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
    </div>
  );
}

Here are a few details on the parameters of the createSlice method:

name: string

A name, used as the ID of the slice in store and also as a prefix for action types of this reducer. This uniquely identifies slices in store.

initialState: any

The initial state for the reducer.

reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>

Reducers in RTK are objects, where keys are used as action types and functions are reducers associated with these types. The key is also used as a suffix for action types, so the final action type becomes ${name}/${actionKey}.

Under the hood, this object is passed to [createReducer](https://redux-toolkit.js.org/api/createReducer), an RTK utility to simplify the definition of reducers. It allows us to define reducers as a lookup table of functions to handle each action type. This helps to avoid the boilerplate code of action creator functions.

It is recommended to use immutable state management in reducers, and Immer is one of the most popular libraries to do so. RTK allows you to mutate state using dot notation and uses Immer under the hood. More precisely, createSlice and createReducer wrap your reducer functions with produce from Immer.

extraReducers

This is also a case reducer function, but for actions other than this slice. Each slice reducer owns its slice in the global store, but it can respond to any action type, including those generated by another slice.

This API will allow us to mutate the state of the current slice on dispatch of actions generated by another slice. The reducer will pass through the same createReducer API, allowing safe mutation.

Accessing data from the store with React-Redux

With React-Redux (already included in the template) we gain access to two important Hooks: useSelector and useDispatch. These Hooks let us read data from the Redux store and dispatch actions from any slice into our components:

// Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  incrementIfOdd,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');
  const incrementValue = Number(incrementAmount) || 0;
  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          aria-label="Set increment amount"
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() => dispatch(incrementByAmount(incrementValue))}
        >
          Add Amount
        </button>
        <button
          className={styles.asyncButton}
          onClick={() => dispatch(incrementAsync(incrementValue))}
        >
          Add Async
        </button>
        <button
          className={styles.button}
          onClick={() => dispatch(incrementIfOdd(incrementValue))}
        >
          Add If Odd
        </button>
      </div>
    </div>
  );
}

Other useful Redux Toolkit APIs

We have discussed two of the major APIs in RTK that are, IMHO, useful for most cases. Under the hood, these APIs make use of other utilities, which are also available individually:

createAction

With Redux, we need to define a constant that represents a type of action, and then a function to create an action of that type. Though this isn’t required by Redux, it helps us keep different store files in sync with each other.

With this API, the hassle of multiple declarations is gone. It takes the action type and returns the action creator for that type. The returned action creator is invoked with an argument, which will be placed as a payload.

Here is a simple example:

const increment = createAction('counter/INCREMENT');
// increment() -> { type: 'counter/INCREMENT' }
// increment(5) -> { type: 'counter/increment', payload: 3 }
// increment.toString() -> 'counter/INCREMENT'
// console.log(increment) -> counter/INCREMENT

For more complex cases, it also accepts the other argument, a function for custom action creation logic. This example makes it easy to grasp:

const addTodo = createAction('todos/ADD, function prepare(text) {
  return {
    payload: {
      text,
      createdAt: new Date().toISOString()
    }
  }
})

console.log(addTodo('Some text'))
/**
 * {
 *   type: 'todos/ADD',
 *   payload: {
 *     text: 'Some text',
 *     createdAt: '2019-10-03T07:53:36.581Z'
 *   }
 * }
 **/

createReducer

As mentioned above, this is one of the special sauces in the createSlice method. It helps us to write a simpler reducer. This removes boilerplate code associated with case reducers and allows us to write reducers as a function lookup table to handle each action type. Using the power of Immer, it makes mutating state more intuitive.

It takes two arguments: first is the initial state, the other is the object mapping from action types to reducers.

A simple counter reducer that may have formerly looked like this:

function counterReducer(state = 0, { type, payload }) {
  switch (type) {
    case 'increment':
      return state + payload
    case 'decrement':
      return state - payload
    default:
      return state
  }
}

Would now look like this:

const counterReducer = createReducer(0, {
  increment: (state, { payload }) => state + payload,
  decrement: (state, { payload }) => state - payload
})

Actions created using the createAction API can be used as keys.

Using Redux-Saga with Redux Toolkit

Redux Saga is a middleware library used to allow a Redux store to interact with resources outside itself asynchronously. This includes making HTTP requests to external services, accessing browser storage, and executing I/O operations. These operations are also known as side effects.

Although asynchronous functions can be created by RTK’s createAsyncThunk, sagas are more powerful and easier to test, but they introduce many new concepts that can be a bit overwhelming if you’re also learning other technologies. This StackOverflow post succinctly explains the difference between Redux Thunk and Redux-Saga.

Run the following command in your terminal to install Redux-Saga and Axios, which will be used for data fetching:

npm install Redux-Saga axios

Now you can use Redux-Saga in your Redux store like so:

// store.js
import { configureStore } from '@reduxjs/toolkit'
import createSagaMiddleware from 'Redux-Saga'
import counterReducer from '../features/counter/counterSlice'
import saga from '../sagas/saga'

let sagaMiddleware = createSagaMiddleware()
const middleware = [sagaMiddleware]

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(middleware),
})

sagaMiddleware.run(saga)

Redux-Saga provides a createSagaMiddle function that we use to instantiate sagaMiddleware. Because the middleware parameter in configureStore API is an array of the existing middleware, we can get the current middleware with the getDefaultMiddleware callback and concatenate our new Saga middleware to the array.

Finally, we use the run function on sagaMiddleware to our saga, which I’ve created in the following path: sagas/saga.js.

Redux-Saga effects

Redux-Saga offers a number of effects, which are instructions given to the middleware to perform certain operations. Some of these include:

  • takeEvery (actionType, saga) spawns a saga on each action dispatched to the store that matches actionType, e.g., FETCH_USER_DETAILS
  • call (fn, …args) creates an effect description that instructs the middleware to call the function fn with args as arguments, if any
  • put: (action) creates an effect description that instructs the middleware to schedule the dispatching of an action to the store

Applying this to our application, we can create a saga that asynchronously fetches a random number from an API and then uses the incrementByAmount reducer from counterSlice to increment the counter value by the value from the external API:

// saga.js
import { call, takeEvery, put } from 'Redux-Saga/effects'
import Axios from 'axios'
import { incrementByAmount } from '../features/counter/counterSlice'
import { sagaActions } from './sagaActions'

// function uses axios to fetch data from our api
let callAPI = async ({ url, method, data }) => {
  return await Axios({
    url,
    method,
    data,
  })
}

export function* fetchNumberSaga() {
  try {
    let result = yield call(() =>
      callAPI({
        url: 'http://www.randomnumberapi.com/api/v1.0/random?min=100&max=1000&count=1',
      })
    )
    yield put(incrementByAmount(result.data[0]))
  } catch (e) {
    yield put({ type: 'NUMBER_SAGA_FAILED' })
  }
}
export default function* rootSaga() {
  yield takeEvery(sagaActions.FETCH_NUMBER_SAGA, fetchNumberSaga)
}

Here, the call effect is used to make a request to an external API, and put is used to update the store with a reducer function.

After the preceding asynchronous request has been made, takeEvery creates a saga that watches for any action with type FETCH_NUMBER_SAGA referenced as sagaActions.FETCH_NUMBER_SAGA from sagaActions.js:

// sagas/sagaActions.js
export const sagaActions = {
  FETCH_NUMBER_SAGA: 'FETCH_NUMBER_SAGA',
}

With our first saga now setup, we can dispatch it from the Counter component as such:

// Counter.js
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  incrementIfOdd,
  selectCount,
} from './counterSlice'
import styles from './Counter.module.css'
import { sagaActions } from '../../sagas/sagaActions'
export function Counter() {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()
  const [incrementAmount, setIncrementAmount] = useState('2')
  const incrementValue = Number(incrementAmount) || 0

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label='Decrement value'
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label='Increment value'
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          aria-label='Set increment amount'
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        {/* buttons */}
        {/* add random number asynchronously with redux saga */}
        <button
          className={styles.button}
          onClick={() => dispatch({ type: sagaActions.FETCH_NUMBER_SAGA })}
        >
          Add Random number with Saga
        </button>
      </div>
    </div>
  )
}

example Redux Toolkit app

Conclusion

As we have seen, Redux Toolkit has eliminated many of the arguments raised against Redux. It also helps to bridge the knowledge gaps around good practices and patterns. It can be super helpful for large-scale applications especially since it can also be used within existing Redux applications.

Whether RTK will be able to resolve all the debate around Redux and its usage is still a question, but without a doubt, it looks like a good leap forward in the right direction. Let us know what you think of it in the comments section.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

3 Replies to “Smarter Redux with Redux Toolkit”

  1. Thanks for writing this article!

    A few quick thoughts:

    – I specifically [chose thunks for use in RTK instead of sagas for a variety of reasons](https://blog.isquaredsoftware.com/2020/02/blogged-answers-why-redux-toolkit-uses-thunks-for-async-logic/). In general, sagas are a great power tool, but most Redux apps don’t actually need them (and especially not just for basic data fetching).
    – Similarly, I’ve never been particularly fond of the whole “container/presentational” concept. It’s not _bad_, but the community has way-over-obsessed on it. That’s why Dan Abramov has said [he no longer supports that idea](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0), and when I rewrite the Redux core tutorial, I’ll be removing that phrasing entirely.
    – “Ducks” are not a competitor to “containers”, because “containers” were always about component structure. Ducks are an alternative to having separate folders for actions/reducers/constants, or even having separate files for those in a single feature folder.
    – I’d suggest expanding the post to show some examples of `createSlice` in action, and especially how Immer makes immutable updates of objects a lot simpler.

  2. Hi Mark, Thanks a lot for taking out time and giving such valuable feedback.

    1) Redux Thunk: I have already mentioned that Thunk is default middleware for async tasks. I specifically mentioned saga since thunk has been discussed in docs and many other tutorials. Also by this I was able to explain option to add middleware.
    Remedy: I am making it more prominent that why I opted for Saga & Thunk is the recommended approach.

    2/3) I mentioned this update on Dan’s article in my previous posts, but as a concept (Segregating store and presentational layer of app), I feel this is still very helpful in many large scale apps. Since I considered example of react-boilerplate I went with CP pattern. CP already had concept of grouping reducer/action/constants based on feature, I feel ducks took that idea a step ahead. I am still open to updates in post if you feel this might lead to any misconceptions in community.
    Remedy: I am rephrasing it upgrade section

    4) Though I am not a big fan of long posts, unless they are coming from you 😉 I will add code of createSlice as a example. About Immer and immutability, post already shed some light on this adding more here will make it a little heavy IMHO.

    Once again thanks for sharing these findings.

  3. Ohh yes, compare how much simpler you can do all of the above with Hookstate: https://hookstate.js.org/ (Disclaimer: I am a maintainer). It will be fast by default without any sorts of selectors. It will be smaller size as the core package is powerful without any extra libs. But it is also extendable with plugins, and standard plugins assist with persistence, state validation, etc… What do you think?

Leave a Reply