Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with Mobile Machine Learning, React, React Native and User Interface Designing.

Smarter Redux with Redux Toolkit

8 min read 2310

Smarter Redux With 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.

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.

Those 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 the container-presentation pattern and Redux setup used with react-boilerplate as a baseline for our transformation. I have been using react-boilerplate for a long time now, and it has helped me bootstrap large-scale applications quickly. It has the following architecture for containers:

  • Redux: For state management
  • Reselect: For selecting a slice out of global store
  • Immer: For handling immutability in stores
  • Redux-Saga: 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. Here, however, we will use Redux-Saga for better understanding of middleware integration with RTK and keeping in sync with our baseline of react-boilerplate.

You can find all the code for this tutorial here on GitHub.

Bootstrapping Redux

One of Redux’s most hotly debated flaws is the effort required to integrate it in an application. As mentioned earlier, the frontend community has complained a lot about the complexity of setting up Redux in their applications.

This is normally what a configureStore file would look like. It includes the integration of Redux DevTools and Redux-Saga middleware.

/**
 * Create the store with dynamic reducers
 */

import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import { createInjectorsEnhancer, forceReducerReload } from 'redux-injectors';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';

export default function configureStore(initialState = {}, history) {
  let composeEnhancers = compose;
  const reduxSagaMonitorOptions = {};

  // If Redux Dev Tools is installed, enable it
  /* istanbul ignore next */
  if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
    /* eslint-disable no-underscore-dangle */
    if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)
      composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});
  }

  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
  const { run: runSaga } = sagaMiddleware;

  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [sagaMiddleware, routerMiddleware(history)];

  const enhancers = [
    applyMiddleware(...middlewares),
    createInjectorsEnhancer({
      createReducer,
      runSaga,
    }),
  ];

  const store = createStore(
    createReducer(),
    initialState,
    composeEnhancers(...enhancers),
  );

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      forceReducerReload(store);
    });
  }

  return store;
}

As you can see, there is some complex code in here that might be helpful for people who want to dive deep and do everything manually to understand exactly what’s happening inside. Besides these power users, most developers who are focused on quick deliverables usually view it as a hassle. With RTK, this is done quite easily.

We made a custom demo for .
No really. Click here to check it out.

configureStore

This is a new API introduced in RTK. In the code above, we called the createStore method to create a Redux store. In addition to accomplishing the same thing as createStore, configureStore also helps us set up other development tools and middlewares. Here is what our updated configureStore file looks like.

Here are the arguments:

reducer<Object>

A key-value pair of reducer functions that forms a root reducer. You might have used the combineReducers method to generate a root reducer with all reducer functions as a value to respective keys. Here is how it is done in our sample project:

/**
 * Combine all reducers in this file and export the combined reducers.
 */

import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';

import history from 'utils/history';
import languageProviderReducer from 'containers/LanguageProvider/reducer';

/**
 * Merges the main reducer with the router state and dynamically injected reducers
 */
export default function createReducer(injectedReducers = {}) {
  const rootReducer = combineReducers({
    language: languageProviderReducer,
    router: connectRouter(history),
    ...injectedReducers,
  });

  return rootReducer;
}

This method calls combineReducers for you internally.

middleware<Array>

An array of middleware methods that adds additional functionality to the store. RTK comes with a few default middlewares that are available via the getDefaultMiddlewares method. Default middlewares include Thunk along with immutableStateInvariant and serializableStateInvariant in debug mode.

devTools<Boolean>

A Boolean value to integrate amazing Redux DevTools with the application. Redux DevTools has a number of valuable features to make your development and debugging faster.

preloadedState<any>

An optional parameter to define the initial state of the Redux store.

enhancers<Array>

An array of enhancers to further strengthen your application. A good example of an enhancer is react-boilerplate’s redux-injector, which allows you to dynamically load Redux reducers and redux-saga sagas as needed instead of loading them all upfront.

Now let’s take a look at how we can create our store using the configureStore API. Here is how our file looks:

/**
 * Create the store with dynamic reducers
 */

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { createInjectorsEnhancer, forceReducerReload } from 'redux-injectors';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';

export default function configureAppStore(initialState = {}) {
  const reduxSagaMonitorOptions = {};
  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
  const { run: runSaga } = sagaMiddleware;

  // sagaMiddleware: Makes redux-sagas work
  const middlewares = [sagaMiddleware];

  const enhancers = [
    createInjectorsEnhancer({
      createReducer,
      runSaga,
    }),
  ];

  const store = configureStore({
    reducer: createReducer(),
    middleware: [...getDefaultMiddleware(), ...middlewares],
    preloadedState: initialState,
    devTools: process.env.NODE_ENV !== 'production',
    enhancers,
  });

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      forceReducerReload(store);
    });
  }

  return store;
}

Creating ducks

Let’s consider an example of a component that pulls all the top articles on HackerNews into our application. This will use Redux Hooks, introduced in v7.1.0. For ease of keeping store logic out of our presentational components, this component will not render anything to the DOM; rather, it will just supply data via Hook and child props. Here is how this would look without RTK, composed of the following files:

  • index.js (exports the container Hook and component)
  • reducer.js
  • selector.js
  • saga.js
  • constants.js
  • actions.js

The filenames more or less indicate their respective functionalities.

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. So our component that provides the data, which is a duck now, will look like this.

As you can see, constants, actions, and reducer files are now gone, and there is a new slice.js file that uses the createSlice method from RTK. This method returns an object with reducers and actions that can be used for injection and/or with other middlewares.

createSlice

As discussed above, our tuple of actions/reducers/constants can be replaced by Redux Toolkit’s createSlice API. It is a helper method to generate a store slice. It takes the name of slice, initial state and reducer function to return 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. Here is how our slice.js looks:

import { createSlice } from '@reduxjs/toolkit';

// The initial state of the HackerNews component
export const initialState = {
  data: [],
  loading: false,
  error: false,
};

const hackerNewsArticlesSlice = createSlice({
  name: 'hackerNewsArticles',
  initialState,
  reducers: {
    fetch(state) {
      state.loading = true;
      state.error = false;
      state.data = [];
    },
    fetchSuccess(state, action) {
      state.data = action.payload.data;
      state.loading = false;
    },
    fetchFailure(state, action) {
      state.error = action.payload.error;
      state.loading = false;
    },
  },
});

export const { name, actions, reducer } = hackerNewsArticlesSlice;

As we can see, createSlice returns an object with a name, reducer, and actions, and these are used in a number of ways. In our example, exports from our slice object are used as follows:

Here are the details of the parameters for this 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, an RTK utility to simplify 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 the safe mutation.

Other useful APIs

We have discussed two of the major APIs in RTK that are, IMHO, most 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.

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.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Full visibility into production React apps

    Debugging React applications can be difficult, especially when users experience issues that are difficult 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 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 User Interface 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