The UI generated by a React, Angular, Vue, or React Native app is a function of its state. For many frontend developers, Redux Toolkit is the perfect tool for the job.
Redux Toolkit is part of the Redux ecosystem. Redux has been the go-to solution for managing the state of complex apps for years now, but Redux Toolkit bridges the gap between Redux and ease of use. For about a year now, Redux Toolkit has been the official recommended approach for using Redux in your app.
In this adoption guide, we’ll explore Redux Toolkit and its features, pros, and cons. We’ll also discuss its use cases and alternatives to help you better assess whether this is the right tool to leverage in your next project.
Redux Toolkit (RTK) is the official, opinionated toolset for efficient Redux development.
Prior to RTK, developers had to manually wire up Redux stores using a lot of boilerplate code. This included setting up reducers, actions, selectors, actions, and middleware just to get a basic store running. RTK handles these painful tasks by providing a set of utility functions that simplify the standard way you’d use Redux.
The Redux team knew that developers had problems with the complexity of Redux. So, they set out to create a solution that would streamline Redux workflow and make state management simpler for developers and React Toolkit was the result.
Initially released in 2019, Redux Toolkit was created to encapsulate best practices, common patterns, and useful utilities to improve the Redux development experience.
Redux Toolkit is a wrapper around the Redux principles that we all know. It provides utilities to simplify the tasks that developers tend to hate. Here is a quick breakdown:
configureStore
function simplifies the process of creating a Redux store with prebuilt configurations and middlewarecreateSlice
function allows you to define reducers and actions simply and without all the old boilerplate codeThe Redux team recently released version 2.0 of Redux Toolkit, which added these features:
combineSlices
method that will lazy load slice reducers for better code splittingcreateAsyncThunk
Let’s be honest: Redux needed to change. It came with excessive boilerplate code, complex setups, and a learning curve that can be steep even for experienced developers in the constantly shifting territory we call frontend development.
Redux Toolkit is the change Redux needed. It adds more to the “pro” side of the pros-and-cons comparison. Here are some reasons to use Redux Toolkit:
createSlice
does all the heavy lifting, saving you time and sanityEven so, Redux Toolkit is not a one-size-fits-all solution. Here’s why you might reconsider using RTK in your app:
Redux Toolkit comes with some powerful features that make using Redux for state management much easier than it was a couple of years ago. Here’s some of the important features you need to know to start using RTK.
Your Redux store holds the single source of truth for your application state. In the past, you would create this store with createStore
. Now, the recommended method is using configureStore
, which will not only create a store for you but will also accept reducer functions.
Here is a basic example of how that configureStore
works:
import { configureStore } from '@reduxjs/toolkit'; import appReducer from 'store/reducers/appSlice'; import cartReducer from 'store/reducers/cartSlice'; const store = configureStore({ reducer: { // Add your reducers here or combine into a rootReducer first app: appReducer, cart: cartReducer }, // We'll look at adding middleware later // middleware: (getDefaultMiddleware) => ... }); // Use these types for easy typing export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>; export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, Action<string>>; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Looking at the example code above, you see that the reducers are imported from “slice” files. These files are where most of the the magic happens. A slice is a function that contains a collection of Redux reducer logic and actions — and more after v2.0 — for a single app feature.
Here is a basic slice:
import { createSlice } from '@reduxjs/toolkit'; interface CartItem { id: string; name: string; price: number; quantity: number; } interface CartState { items: CartItem[]; total: number; } const initialState: CartState = { items: [], total: 0, }; const cartSlice = createSlice({ // Name the slice name: 'cart', // Set the initial state initialState, // Add reducer actions to this object reducers: { addItem(state, action: { payload: CartItem }) { const newItem = action.payload; state.items.push(newItem); state.total += newItem.price; }, removeItem(state, action: { payload: string }) { const itemId = action.payload; const itemIndex = state.items.findIndex((item) => item.id === itemId); if (itemIndex !== -1) { state.items.splice(itemIndex, 1); state.total -= state.items[itemIndex].price; } }, updateQuantity(state, action: { payload: { itemId: string; newQuantity: number } }) { const { itemId, newQuantity } = action.payload; const item = state.items.find((item) => item.id === itemId); if (item) { item.quantity = newQuantity; state.total += (newQuantity - item.quantity) * item.price; } }, }, }); // Export actions for use in the app export const { addItem, removeItem, updateQuantity } = cartSlice.actions; // Export reducer to add to store (the first example) export default cartSlice.reducer;
The code above will allow us to update the Redux state in the app. We just have to dispatch the actions we exported from the slice file. But what if we want to populate the state with data from an API call? That’s pretty simple too.
Redux Toolkit comes with Redux Thunk. If you’re using v2.0 or higher, you can add the thunks directly to your slice. A thunk in Redux encapsulates asynchronous code. Here’s how you use them in a slice, with comments explaining the important parts:
const cartSlice = createSlice({ name: 'cart', initialState, // NOTE: We changed this to a callback to pass create in. reducers: (create) => ({ // NOTE: Standard reducers will now have to use callback syntax. // Compare to the last example. addItem: create.reducer(state, action: { payload: CartItem }) { const newItem = action.payload; state.items.push(newItem); state.total += newItem.price; }, // To add thunks to your reducer, use create.AsyncThunk instead of create.reducer // The first parameter create.AsyncThunk is the actual thunk. fetchCartData: create.AsyncThunk<CartItem[], void, {}>( 'cart/fetchCartData', async () => { const response = await fetch('https://api.example.com/cart'); const data = await response.json(); return data; }, // The second parameter of create.AsyncThunk is an object // where you define reducers based on the state of the API call. { // This runs when the API is first called pending: (state) => { state.loading = true; state.error = null; }, // This runs on an error rejected: (state, action) => { state.loading = false; state.error = true; }, // This runs on success fulfilled: (state, action) => { state.loading = false; state.items = action.payload; state.total = calculateTotal(state.items); // Define a helper function }, }, ), }), });
Most of the time, you don’t want the whole state object from a slice. In fact, I am not sure why you would want the whole thing. This is why you need selectors, which are simple functions that accept a Redux state as an argument and return data that is derived from that state.
In Redux Toolkit v2.0 and newer, you can add selectors directly to your slice. Here’s how:
//...imports, types, and initialState in above code const cartSlice = createSlice({ name: 'cart', initialState, reducers: { //... standard reducers in above code }, selectors: { selectItems: state => state.items, selectTotal: state => state.total, }, }); // Exporting selectors const { selectItems, selectTotal } = cartSlice.selectors;
Then we can import these selectors into other parts of the app:
import { selectItems, selectTotal } from 'store/reducers/cartSlice'; const itemsInCart = selectItems();
Redux Toolkit provides a getDefaultMiddleware
function that returns an array of the middleware that are applied by configureStore
. This default set of middleware includes:
actionCreatorCheck
: Makes sure dispatched actions are created using createAction
for consistencyimmutableCheck
: Warns against mutations in the state objectthunk
: Enables asynchronous operations and side effects by allowing functions within actions to dispatch other actions or interact with external APIsserializableCheck
: Warns if non-serializable values are detected in stateYou can customize the middleware by passing a middleware
option to configureStore
, with a callback function that receives the default middleware as an argument. Here is an example:
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; // The middleware we want to add import logger from 'redux-logger'; const store = configureStore({ reducer: rootReducer, // Using a callback function to customize the middleware middleware: (getDefaultMiddleware) => getDefaultMiddleware() // You can disable or configure the default middleware .configure({ serializableCheck: false }) // You can add more middleware .concat(logger), }); export default store;
In Redux Toolkit v2.0 and newer, this middleware can be dynamic, which means you can add it at runtime. Here’s how you do that:
import { configureStore, getDefaultMiddleware, createDynamicMiddleware } from '@reduxjs/toolkit'; export const dynamicMiddleware = createDynamicMiddleware(); const store = configureStore({ reducer: rootReducer, // Using a callback function to customize the middleware middleware: (getDefaultMiddleware) => getDefaultMiddleware() .prepend(dynamicMiddleware.middleware), });
Adding the above code to your store sets the store up to accept dynamic middleware. Now in the rest of our app, we can add middleware at runtime like this:
import { dynamicMiddleware } from 'store'; import logger from 'redux-logger'; if (someCondition) { dynamicMiddleware.addMiddleware(logger); }
I wouldn’t say the Redux DevTools extension, either the Chrome or Firefox version, is necessary for developing with Redux. However, it’s pretty close — debugging your application’s Redux state with console.log
statements is possible, but I would recommend Redux DevTools over the console.log
approach.
The good news is that when you use configureStore
, it automatically sets up Redux DevTools for you. When using createStore
, you have to configure Redux to use this extension yourself.
Redux Toolkit is really versatile and can be used for a wide range of applications. Let’s talk about some places where it shines.
The biggest benefit of Redux is managing state across growing, complex apps consistently. Redux Toolkit makes it simpler to use Redux for this purpose. It’s ideal for:
Redux Toolkit comes with helpers for complex workflows. Features like createAsyncThunk
and createEntityAdaptor
will speed up implementing:
An ecommerce app may use these features to update cart quantities optimistically after adding items rather than waiting on API responses.
If you are already using Redux, it’s time to move to Redux Toolkit, as it’s now the official way to use Redux. The migration process is relatively simple. You can continue to use vanilla Redux as you migrate to RTK one slice at a time.
Redux Toolkit did have some issues in the past that made it incompatible with modern JavaScript features, including:
exports
field in the package.json
file.mjs
import: Importing RTK in a .mjs
file failed due to the use of module
but no exports
fieldnode16
module resolution: RTK didn’t work with the new TypeScript module resolution optionThe Redux team took action to resolve these limitations in v2.0. Here is what they changed:
exports
field in package.json
: Modern ESM build is now the primary artifact, while CJS is included for compatibility. This defines which artifacts to load and ensures proper usage in different environments./dist/
, simplifying the package structure. TypeScript support is also enhanced, with the minimum supported version being v4.7dist/$PACKAGE_NAME.browser.mjs
— is still available for script tag loading via UnpkgFor more details on these changes, including the reasons for implementing them, check out my experience modernizing packages to ESM.
Well, we already mentioned why you would and why you wouldn’t use Redux Toolkit. Now let’s get to the fun part: comparing Redux Toolkit to similar libraries — and your opportunity to yell at me about these comparisons in the comments 😅
First, a disclaimer: I only have limited experience with these libraries and am trusting the generic “community opinions” and documentation to flesh some of these comparisons out.
First, let’s compare the features of these libraries:
Feature | Redux Toolkit | MobX | Zustand | Jotai |
---|---|---|---|---|
Data immutability | Enforced | Optional | Enforced | Enforced |
Time travel | Available through DevTools | Built-in | Available through Zustand Devtools | – |
Server-side rendering | Supported | Supported | Supported | Supported |
TypeScript support | Excellent | Excellent | Good | Excellent |
Learning curve | Moderate | Easy | Easy | Easy |
Next, let’s compare them in terms of performance:
Library | Benchmarks | Considerations |
---|---|---|
Redux Toolkit | Generally performs well, especially with optimizations | Overhead of createSlice and reducers |
MobX | Can be faster for simple applications, but complex reactivity can lead to performance issues | Requires careful tracking of dependencies to avoid performance bottlenecks |
Zustand | Can be lightweight and performant, but may not scale well for large state trees | Requires manual configuration for complex state management |
Jotai | Often praised for its performance, particularly with smaller state slices | Less mature ecosystem and may require more setup for complex use cases |
It’s also important to consider the community supporting each library:
Library | GitHub stars | Active contributors |
---|---|---|
Redux Toolkit | 10.3k+ (60.3+ for Redux) | 339+ (979+ for Redux) |
MobX | 27k+ | 315+ |
Zustand | 40.3k+ | 223+ |
Jotai | 16.7k+ | 178+ |
Finally, let’s compare their documentation and resources:
Library | Documentation | Tutorials/Examples |
---|---|---|
Redux Toolkit | Extensive and well-maintained | Numerous community resources and tutorials |
MobX | Comprehensive, but slightly less clear than RTK | Fewer tutorials and community resources |
Zustand | Good documentation, but less comprehensive than RTK | Growing community resources and tutorials |
Jotai | Concise and well-written, but limited compared to RTK | Fewer community-created resources and tutorials |
Overall, Redux Toolkit balances features, performance, and community support, making it a strong choice for many projects.
MobX offers an easier learning curve and potential performance benefits for simpler applications, but requires careful handling of reactivity. Zustand is lightweight and performant for smaller state needs, while Jotai is gaining traction but has a smaller ecosystem.
Here’s a summary table comparing these libraries:
Feature | Redux Toolkit | MobX | Zustand | Jotai |
---|---|---|---|---|
Data immutability | Enforced | Optional | Enforced | Enforced |
Time travel | DevTools | Built-in | DevTools | – |
Server-side rendering | Supported | Supported | Supported | Supported |
TypeScript support | Excellent | Excellent | Good | Excellent |
Learning curve | Moderate | Easy | Easy | Easy |
Performance | Good (may require optimization) | Can be faster for simple apps | Lightweight, may not scale well | Performant, setup required |
Community | Large and active | Smaller but active | Growing | Smaller but active |
Documentation | Extensive and well-maintained | Comprehensive | Good | Concise |
Redux Toolkit is the official, standard way to build Redux applications today. It eliminates the boilerplate that has traditionally made Redux somewhat of a pain, while retaining and enhancing its benefits for state management.
Alternatives to RTK include MobX, which offers simplicity and potential performance benefits, and Zustand, which is lightweight and performant for smaller state needs. Jotai is also gaining traction with its concise approach but has a less mature ecosystem. And, of course, not every app needs a state management library.
However, while Redux has been around for a while, it still stands out for its strong features, active community, and documentation. Working with Redux is a breeze thanks to Redux Toolkit, making it an easy choice for many types of projects.
I hope this adoption guide was helpful. If you have any thoughts or further questions, feel free to comment below.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
2 Replies to "Redux Toolkit adoption guide: Overview, examples, and alternatives"
How does Redux Toolkit simplify the process of setting up Redux stores compared to traditional methods?
Please check out the “What is Redux Toolkit?” and “Why use Redux Toolkit?” sections of this guide, which may provide the answer you’re looking for. If there’s anything else we can help with, let us know! Thanks for reaching out.