Table Of Contents
- Introduction
- Installation
- Create Redux Store
- Create Slices
- Add Reducers to Store
- Performing Asynchronous Logic and Data Fetching
- Conclusion
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
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: {},
})
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.
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'
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...
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
})
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
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!
Top comments (9)
Just two nitpicks there :)
useSelector
anduseDispatch
.connect
is pretty much a legacy api.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!
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.
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.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.
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.
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.
Thanks again for the awesome info! @phryneas
I have dropped my use of
connect
since your initial comment, and it has made my life easier no doubt!