24
How to add Redux Toolkit to a React-Redux application ⚛️
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!
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.
First and foremost, install the Redux Toolkit package in your React-Redux application:
npm install @reduxjs/toolkit react-redux
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: {},
})
By creating the Redux store, you are now able to observe the store from the Redux Devtools extension while developing.
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')
)
And there you have it, a beautifully set up Redux Store.
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'
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 },
}
}
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' })
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) => {})
})
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
}
}
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
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.
-
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.
-
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...
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
})
If you were to have more than one slice, it would look like this:
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
})
You can take away that the reducers...
"Own" a piece of state, including what the initial value is.
Define how that state is updated.
Define which specific actions result in state updates
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.
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!
24