Sinan Aksay smiling with cool shades

on July 06, 2020

Modern Redux with Redux Toolkit

redux6 min read

Redux Toolkit (RTK for short) is the recommended toolset by Redux Team for writing Redux code. RTK provides simple utility functions to write cleaner, easier and reusable code. Out of the box, RTK comes with useful Redux packages like Redux Thunk and Immer.

In this article, I’ll walk you through how to implement Redux Toolkit on a React app that already uses Redux. I’ll use an app called ColorsApp, it’s a small project I created during a live stream earlier1. This article assumes you have an understanding of both React and Redux.

In case you prefer to read the code on your editor, you can clone pre-redux-toolkit and redux-toolkit-implementation branches from GitHub and compare the code.

Let’s get to it.

Installation

For existing apps, you can install Redux Toolkit by running this command:

yarn add @reduxjs/toolkit

## OR

npm install @reduxjs/toolkit

For new apps, RTK recommends using their official template.

configureStore()

My original code to create the Redux store looks like this:

// old store.js

import { applyMiddleware, compose, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'

import rootReducer from './reducers'

const middlewareEnhancer = applyMiddleware(thunk)
const composedEnhancers = composeWithDevTools(middlewareEnhancer)

const store = createStore(rootReducer, undefined, composedEnhancers)

export default store

I’m using Redux Devtools Extension and Redux Thunk middleware with my store.

RTK provides a function called configureStore to create a Redux store. Let’s create the store the RTK way:

// new store.js

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({ reducer: rootReducer })

export default store

Much cleaner, huh? configureStore has both Redux Thunk and Redux Devtools Extension by default, so there is no need to implement them.

Meet slices 🍕

Let’s look at the login action and reducer.

// actions/login.js

export function userLoggedIn(userEmail) {
  return {
    type: 'USER_LOGGED_IN',
    payload: {
      userEmail
    }
  }
}
// reducers/login.js

const defaultState = {
  isUserLoggedIn: false,
  userEmail: null
}

const loginReducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'USER_LOGGED_IN':
      return {
        ...state,
        userEmail: action.payload.userEmail,
        isUserLoggedIn: true
      }

    default:
      return state
  }
}

export default loginReducer

While using Redux, we slice the application state to small chunks and create a reducer for each slice, then merge all reducers with combineReducers. RTK’s loginSlice function makes the creation of a slice a lot easier:

// Login/loginSlice.js

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  isUserLoggedIn: false,
  userEmail: null
}

const loginSlice = createSlice({
  name: 'login',
  initialState,
  reducers: {
    userLoggedIn: (state, action) => ({
      ...state,
      userEmail: action.payload.userEmail,
      isUserLoggedIn: true
    })
  }
})

export default loginSlice

createSlice() accepts a single object with name, initialState and reducers keys. reducers here is the equivalent of the switch statement we would use on a traditional reducer. It’s an object with action types as keys (more on this later) and reducer logic as values.

To pass a slice to combineReducers, we use its reducer property.

import loginSlice from 'Login/loginSlice'

export default combineReducers({
  login: loginSlice.reducer,
  // more reducers...
})

As you probably noticed, loginSlice lives inside the /Login directory, where the relevant component is. Structuring files by feature and placing all relevant files under a feature folder is recommended by Redux Team.

But wait, where do we define action creators with this approach? 🤔

We don’t. createSlice automatically generates the actions for you. So when you dispatch the userLoggedIn action from above you just write:

import loginSlice from './loginSlice'

dispatch(loginSlice.actions.userLoggedIn(email))

…and email passed to userLoggedIn can be accessed with action.payload in the reducer. How cool is that? Action types are generated using the slice name and the key in the reducers object. So for this example, you’ll see login/userLoggedIn in your Dev Tools2.

Mutating the state

RTK uses a package called Immer which allows you to write mutations and still have an immutable state! So as an alternative to returning copy of the state, we can also do this:

const loginSlice = createSlice({
  ///...
  reducers: {
    userLoggedIn: (state, action) => {
      state.userEmail = action.payload
      state.isUserLoggedIn = true
    }
  }
})

What about thunks?

Here is an async action I have on the ColorsApp:

// actions/colors.js

export function getColors() {
  return async (dispatch) => {
    dispatch({
      type: 'GET_COLORS_STARTED'
    })

    try {
      const res = await fetch('https://reqres.in/api/colors')
      const { data } = await res.json()

      dispatch({
        type: 'GET_COLORS_SUCCESS',
        payload: data
      })
    } catch (error) {
      dispatch({
        type: 'GET_COLORS_FAILED',
        payload: error
      })
    }
  }
}

RTK’s solution for async actions is createAsyncThunk() and it looks like this:

// colorsListSlice.js

export const fetchColorList = createAsyncThunk('colorList/fetchColorList', async () => {
  const res = await fetch('https://reqres.in/api/colors')
  const { data } = await res.json()

  return data
})

The cool thing about createAsyncThunk() is, it has “started”, “success” and “failed” action creators built-in. So when you dispatch fetchColorList action:

  • It’ll dispatch colorList/fetchColorList/pending
  • Then if the promise resolves, it’ll dispatch colorList/fetchColorList/fulfilled
  • If promise is rejected, instead it’ll dispatch colorList/fetchColorList/rejected

Even if it uses a different naming convention for action types, the options are the same as my original code.

We’ll handle these actions in our slice like this:

// colorsListSlice.js

const colorListSlice = createSlice({
  name: 'colorList',
  initialState,
  extraReducers: {
    [fetchColorList.pending]: (state) => {
      state.error = null
      state.isLoading = true
      state.colors = []
    },
    [fetchColorList.fulfilled]: (state, action) => {
      state.error = null
      state.isLoading = false
      state.colors = action.payload
    },
    [fetchColorList.rejected]: (state, action) => {
      state.isLoading = false
      state.error = action.payload
    }
  }
})

Notice this time we used extraReducers instead of reducers. extraReducers is for the actions not generated by our slice. We can still modify our state thanks to Immer.

All done! As you can see by using Redux Toolkit, not only did we get rid of lots of boilerplate, but also we got automatically generated action types with better naming, got a better file structure and we don’t need to worry about mutating the state in reducers anymore.

Happy coding! 🍕


  1. ^

    The livestream was about creating a React app using Redux and React Router. You can find it here. It’s in Turkish though.

  2. ^

    Using ‘domain/eventName’ format for action types is another recommendation by Redux.

Discuss on TwitterEdit on GitHub

Enjoyed this article?

Get new ones
in your inbox!

No spam. Unsubscribe whenever.