Getting Started with Redux, Part 4

Christopher Leja
6 min readOct 23, 2020

Welcome back to my series exploring state management in React applications with Redux. Here are part 1, part 2, and part 3 case you missed those (or just want to revisit). This is going to be the (hopefully) thrilling conclusion of this series.

By now, you’ve seen essentially how Redux works — how we create and dispatch actions to our reducers and update our store. We’ve talked about React-Redux, adding middleware, combining reducers. Hopefully at this point you understand generally how Redux works and why it’s useful.

But what are some of the downsides to Redux? There are a handful of common complaints — for starters, it’s a lot of boilerplate code you have to write before it can tangibly help you, and that can be annoying. There are also a lot of packages to add to make it truly functional, which often frustrates people. Plus, sometimes configuring a store is just complicated. When I began working with Redux, these were all frustrations I had with it. Fortunately, it seems I wasn’t alone, and the Redux team decided to make it even easier for us. And thus, the Redux Toolkit was born.

Artist’s rendition of the Redux Team presenting Redux Toolkit to the world

So what does the Redux Toolkit give us? Basically, it simplifies the process of creating and connecting to Redux, allowing you to get to building your application faster. Today, I’m going to explain a few of its key functions — namely configureStore, createReducer, createAction, and, my personal favorite, createSlice.

So let’s dive in. First, we install the library with npm or yarn:

npm install @reduxjs/toolkit
//or
yarn add @reduxjs/toolkit

And just like that, we’re ready to get started. So first, let’s talk about the configureStore function. As you might imagine, configureStore is used to create the Redux store — so what does it offer that the original createStore function doesn’t?

The simplest answer is that configureStore hides a lot of the work from you. Let’s do a side by side comparison (from the docs) of what setting up a store could look like:

//THE MANUAL WAYimport { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
// here we have to manually combine our middlewares
const middlewares = [loggerMiddleware, thunkMiddleware]
// and then combine them with applyMiddleware
const middlewareEnhancer = applyMiddleware(...middlewares)
// which we then pass into the enhancers array
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
// before we pass them to the composeWithDevToolsFunction
const composedEnhancers = composeWithDevTools(...enhancers)
// since we have to pass all of that information as a single parameter to our store.
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}

Compare that to how the same code would look using Redux toolkit:

//WITH REACT TOOLKIT
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {

// with configureStore, we pass a configuration object instead of
// limited parameters.
const store = configureStore({
reducer: rootReducer,
middleware: [loggerMiddleware, ...getDefaultMiddleware()],
preloadedState,
enhancers: [monitorReducersEnhancer]
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store}

In this new version, rather than passing in strictly ordered parameters, we pass configureStore a configuration object. Also, note how in middleware and enhancers, we are passing in arrays — under the hood, Redux Toolkit will still do all the steps from the above example, but we don’t have to worry about implementing it ourselves. The result is cleaner, easier to type, and much easier to read.

Also, notice the getDefaultMiddleware function in the middleware array — this is built-in to Redux Toolkit. If you don’t specify any middleware, configureStore will still automatically give you access to some — thunk, for example, is built in. In this example, I called getDefaultMiddleware directly because I wanted to add the loggerMiddleware function to the list, but if you just wanted to use the built-in middleware, you could simply leave that key off entirely. In addition to Thunk, Redux Toolkit automatically offers middleware to throw an error if you directly mutate state or if your state is not serializable. It also allows you by default to connect with Redux Devtools which are incredibly useful.

Another way Redux Toolkit hides some complexity from us is by automatically calling combineReducer.

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './users/reducer';
import postReducer from './posts/reducer';
const reducer = {
users: userReducer,
posts: postReducer,
}
const preloadedState = {
// whatever initial state we need for our app
}
// configureStore will automatically call combineReducer for us
// so this store will work perfectly.
const store = configureStore({
reducer,
preloadedState,
})
//this store will also have access to middleware by default

So now, let’s move on to createAction. Unsurprisingly, createAction is a very simple interface for making action creators. Here’s a side by side comparison between the approaches.

import { createAction } from '@reduxjs/toolkit';// With plain reduxconst INCREMENT = "counter/increment";const increment = (amount) => ({ 
type: INCREMENT,
payload: amount,
})
// with redux toolkitconst decrement = createAction('counter/decrement'); // returns { type: "counter/decrement", payload: undefined }doTheThing(1) // returns { type: "counter/decrement", payload: 1 }

It’s not a huge change, but it does simplify our code a little bit. Basically, createAction takes an action type as an argument and returns an action creator — but with the toolkit, you can easily do it in one simple line.

If you want to further customize your action creators, createAction also allows for an optional second argument: a “prepare callback” that lets you add extra information to the payload of your actions. Here’s how that looks:

import { createAction } from '@reduxjs/toolkit'const doTheThing = createAction('doTheThing', function prepare(thing) {
return {
payload: {
thing,
createdAt: new Date().toISOString(),
},
}
});
console.log(doTheThing("Something"))
// {
// type: "doTheThing";
// payload: {
// thing: "Something",
// createdAt: 2020-10-22T23:57:41.506Z,
// }
// }

In this case, now there’s a createdAt key on this action’s payload. Ta-da!

Anyways, now let’s move on to reducers, and how the createReducer can make them a little easier for us. Again, we’ll go with a side by side comparison to illustrate the differences.

// WITHOUT TOOLKIT:function counterReducer(state = 0, action) {
switch (action.type) {
case 'increment':
return { ...state, value: state.value + 1 }
case 'decrement':
return { ...state, value: state.value - 1 }
default:
return state
}
}
// WITH TOOLKIT:
import { createReducer, createAction } from '@reduxjs/toolkit';
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
export default counterReducer = createReducer(0, { // we use the type from our actions to dynamically render keys
// and the values are the function we want to call
[increment.type]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload,
})

As you can see, there are a couple of key differences. For one thing, with Redux Toolkit, we don’t have to write immutable code — under the hood, Redux Toolkit uses immer to translate your code to be immutable, which means you don’t have to worry about it. This lets you write clearer, more direct code, and eliminates some common mistakes people make while spreading nested state. Another benefit is not having to worry about returning default state or switch/case statements, which can also lead to annoying bugs in your app.

The last part of Redux Toolkit I want to cover today is my personal favorite part of the library. The createSlice function can create your actions and reducers in one fell swoop. Here’s how it works:

import { createSlice } from '@reduxjs/toolkit';const slice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
// the count argument is state--I just renamed it for clarity
increment: (count, action) => count + action.payload,
decrement: (count, action) => count - action.payload
}
})
// our slice automatically creates actions and a reducer for us
// which we can access through slice.actions and slice.reducer
export const { increment, decrement } = slice.actions;
export default slice.reducer

This slice object will automatically create actions for the keys we listed in the reducers object. In our example, the actions will read ‘counter/increment’ and ‘counter/decrement’ which means we’ll always have clear action names available.

Thank you so much for reading! I hope I’ve converted you to the good gospel of Redux Toolkit. It truly simplifies the process of setting up Redux so much, and I am a huge fan.

--

--