DEV Community

Cover image for How to add Redux Toolkit to a React-Redux application ⚛️
Maxine
Maxine

Posted on • Edited on

How to add Redux Toolkit to a React-Redux application ⚛️

Table Of Contents

Over the last couple days I realized I wasn't alone in learning the wonders of Redux Toolkit. So for those of you that are in the same boat as me, get ready for some ducks!

Introduction

Redux Toolkit is package that was built on top of Redux an open-source JS library for managing application state. The package allows the user to avoid unnecessary boilerplate code, and supplies APIs that make applications DRYer and more maintainable. If you'd like to read more about Redux Toolkit and its features, I have another blog post available here.

Today we'll be focusing on how to implement Redux toolkit in a React-Redux application.

Installation

First and foremost, install the Redux Toolkit package in your React-Redux application:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Create Redux Store

Create a file named src/redux/store.js. I choose to name the folder containing my store and slices "redux", in the documentation you will see it named "app", the convention is your choice. Inside the store.js file, import the configureStore() API from Redux Toolkit. You're simply just going to start by creating and exporting an empty Redux store:

// src/redux/store.js

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})
Enter fullscreen mode Exit fullscreen mode

By creating the Redux store, you are now able to observe the store from the Redux Devtools extension while developing.

Console Gif

After the store is created, you must make it available to your React components by putting a React-Redux Provider around your application in src/index.js. Import your newly created Redux store, put a Provider around your App, and pass the store as a prop:

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './redux/store' // import your store
import { Provider } from 'react-redux' // import the provider

ReactDOM.render(
  <Provider store={store}> // place provider around app, pass store as prop
    <App />
  </Provider>, 
  document.getElementById('root')
)
Enter fullscreen mode Exit fullscreen mode

And there you have it, a beautifully set up Redux Store.

Create Slices

To create your first slice, we'll be adding a new file generally named after what you will be performing the actions on, or the action itself. For this example, let's say we are creating an app that allows a user to create posts. I would then create a file named src/redux/PostSlice.js. Within that file, you would then import the createSlice API from Redux Toolkit like so:

// src/redux/PostSlice.js

import { createSlice } from '@reduxjs/toolkit'
Enter fullscreen mode Exit fullscreen mode

A slice requires a string name to identify the slice, an initial state value, and one or more reducer functions, defining how the state can be updated. After creating the slice, you can export the already generated Redux action creators and reducer function for the entire slice.

Redux requires that we write all state updates immutably, it does this by making copies of data and updating the copies. But, Redux Toolkit's createSlice and createReducer APIs use Immer ,a package that allows you to work with immutable state, allowing you to write "mutating" update logic that then becomes correct immutable updates. Right now you're probably use to your action creators looking something like this:

function addPost(text) {
  return {
    type: 'ADD_POST',
    payload: { text },
  }
}
Enter fullscreen mode Exit fullscreen mode

But Redux Toolkit provides you a function called createAction, which generates an action creator that uses the given action type, and turns its argument into the payload field. It also accepts a "prepare callback" argument, allowing you to customize the returning payload field:

const addPost = createAction('ADD_POST')
addPost({ text: 'Hello World' })
Enter fullscreen mode Exit fullscreen mode

Redux reducers search for specific action types to know how they should update their state. While you may be use to separately defining action type strings and action creator functions, the createAction function cuts some of the work out for you.

You should know that, createAction overrides the toString() method on the action creators it generates. This means that in some clauses, such providing keys to builder.addCase, or the createReducer object notation. the action creator itself can be used as the "action type" reference. Furthermore, the action type is defined as a type field on the action creator.

Here is a code snippet from Redux Toolkit Documentation:

const actionCreator = createAction('SOME_ACTION_TYPE')

console.log(actionCreator.toString())
// "SOME_ACTION_TYPE"

console.log(actionCreator.type)
// "SOME_ACTION_TYPE"

const reducer = createReducer({}, (builder) => {
  // actionCreator.toString() will automatically be called here
  // also, if you use TypeScript, the action type will be correctly inferred
  builder.addCase(actionCreator, (state, action) => {})

  // Or, you can reference the .type field:
  // if using TypeScript, the action type cannot be inferred that way
  builder.addCase(actionCreator.type, (state, action) => {})
})
Enter fullscreen mode Exit fullscreen mode

Here's how our example PostSlice would look if we were to use the "ducks" file structure...

// src/redux/PostSlice.js

const CREATE_POST = 'CREATE_POST'

export function addPost(id, title) {
  return {
    type: CREATE_POST,
    payload: { id, title },
  }
}

const initialState = []

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case CREATE_POST: {
      // Your code
      break
    }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

While this definitely simplifies things, you would still need to write actions and action creators manually. To make things even easier, Redux toolkit includes the a createSlice function that will automatically generate the action types/action creators for you, based on the names of the reducer functions provided.

Here's how our updated posts example would look with createSlice:

// src/redux/PostSlice.js

import { createSlice } from '@reduxjs/toolkit'

const postsSlice = createSlice({
  name: 'posts',
  initialState: [],
  reducers: {
    createPost(state, action) {}
  },
})

const { createPost } = postsSlice.actions
export const { createPost } = actions
export default PostSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Slices defined in this manner are similar in concept to the "Redux Ducks" pattern. However, there are a few things to beware of when importing and exporting slices.

  1. Redux action types are not meant to be exclusive to a single slice.

    • Looking at it abstractly, each slice reducer "owns" its own piece of the Redux state. But, it should be able for listen to any action type, updating its state accordingly. For example, many different slices may have a response to a "LOG OUT" action by clearing or resetting data back to initial state values. It's important to remember this as you design your state shape and create your slices.
  2. JS modules can have "circular reference" problems if two modules try to import each other.

    • This can result in imports being undefined, which will likely break the code that needs that import. Specifically in the case of "ducks" or slices, this can occur if slices defined in two different files both want to respond to actions defined in the other file. The solution to this is usually moving the shared/repeated code to a separate, common, file that both modules can import and use. In this case, you might define some common action types in a separate file using createAction, import those action creators into each slice file, and handle them using the extraReducers argument.

This was personally an issue I had when first using Redux Toolkit, and let's just say it was a very long 8 hours...

Add Reducers to Store

Once you created your slice, and read/signed the terms and conditions listed above, you can import your reducers in the store. Redux state is typically organized into "slices", defined by the reducers that are passed to combineReducers:

// src/redux/store.js

import { configureStore } from '@reduxjs/toolkit'
import postsReducer from './postSlice'

const rootReducer = combineReducers({
  posts: postsReducer
})
Enter fullscreen mode Exit fullscreen mode

If you were to have more than one slice, it would look like this:

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer
})
Enter fullscreen mode Exit fullscreen mode

You can take away that the reducers...

  1. "Own" a piece of state, including what the initial value is.

  2. Define how that state is updated.

  3. Define which specific actions result in state updates

Performing Asynchronous Logic and Data Fetching

You also may be asking how to import and use this in your actual components, which is where useDispatch, useSelector, connect, and mapDispatchToProps comes in play.

If you are looking to include async logic into your code, you will have to use middleware to enable async logic, unless you would like to write all that lovely code yourself.

Redux store alone doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. So, any asynchronicity has to happen outside the store. If you are looking to implement this into your application, I would to look into this documentation and utilizing createAsyncThunk.

Conclusion

You have successfully transitioned over from vanilla Redux to Redux Toolkit! You probably have some cleaning up to do throughout your application, as your code has been greatly reduced. While this definitely does not cover the entirety of the package, it should at least get you started!

I sincerely hope this article has aided you in your transition from vanilla Redux to Redux Toolkit. I would appreciate any feedback you have, and feel free to share your applications using Redux Toolkit! Happy Coding!

Sources/ Supplemental Resources:

Top comments (9)

Collapse
 
phryneas profile image
Lenz Weber

Just two nitpicks there :)

  • it's slice, not slicer - although a lot of people seem to call it that 😅
  • nowadays we recommend to use the react-redux hooks useSelector and useDispatch. connect is pretty much a legacy api.
Collapse
 
maxinejs profile image
Maxine

Thank you so much for the feedback! I have seen them called slices and slicers, (I just went with slicers for personal preference), but good to know! And I also didn't know connect was considered a "legacy" API, so I will have to refresh my knowledge on useSelector/Dispatch. But, again thank you!

Collapse
 
lexiebkm profile image
Alexander B.K.

Connect is a legacy API ? Then, how we can use React-Redux when we still have to work with class components ? Connect is still provided for working with class components.

Collapse
 
phryneas profile image
Lenz Weber • Edited

It is there and it will not go away in the next version of React-Redux. But due to declining ecosystem support for class components of essentially all new libraries (and forseeable limited usability in React 18), class components themselves are essentially a legacy api.

I would not recommend anyone to write new class components at this point and so I would also not recommend anyone to write a new usage of connect, since in function components hooks are a lot easier to reason about for most people.

Thread Thread
 
lexiebkm profile image
Alexander B.K.

Sure, as React team also encourage us to use Hooks in new projects; hence we can also use Hooks API of React-Redux, instead of Connect. But, like I said, in case of existing codebases that still use class components, we still have to use Connect API.
I myself, by the suggestion of React team, will start to use Hooks in new projects, from which I can use Hooks API from React-Redux.
I am also now considering to use useReducer in combination with Context as an alternative to Redux, as mentioned in the following article : dev.to/kendalmintcode/replacing-re...
I will try this approach, although I am not sure if it will fit to my need.

Thread Thread
 
phryneas profile image
Lenz Weber

I said multiple times, we are not going to remove it and using it is safe. We still consider it "legacy" in the sense that most users will probably never connect a new component again and over a longer time it's usage might fade away. We will keep it running but will probably never add any new feature to it.

As for context for state, I would advise against that: Context is a dependency injection mechanism meant for seldomly changing values - it is not meant for something like state (we tried that in react-redux 6 and performance was very bad) that might change a lot, especially since it does not support partial subscriptions. Give dev.to/javmurillo/react-context-al... a read.

Thread Thread
 
lexiebkm profile image
Alexander B.K. • Edited

Thanks for telling me about caveats of Context for using it as global state management. That is actually I have been wondering so far.
Now that I have found that you are an expert, who is a contributor of Redux Toolkit, I should take your advice.
I am still learning Redux with RTK, including RTK query as well as React-Redux. There are a lot of things to learn of these Redux technology that need time/patience to use it correctly, but I hope it will pay of in the long run for me.

Thread Thread
 
maxinejs profile image
Maxine

Thanks again for the awesome info! @phryneas

Thread Thread
 
maxinejs profile image
Maxine

I have dropped my use of connect since your initial comment, and it has made my life easier no doubt!