Written by Stephan Miller✏️
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.
What is Redux Toolkit?
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.
How does Redux Toolkit work?
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:
- Simplified store setup: RTK's
configureStore
function simplifies the process of creating a Redux store with prebuilt configurations and middleware - Automatic reducer and action creation: The
createSlice
function allows you to define reducers and actions simply and without all the old boilerplate code - Intuitive state updates: RTK integrates with the Immer library, which means you can write to state without handling immutability manually
- Middleware: RTK comes with Redux Thunk for asynchronous actions and Reselect for optimized selector functions
- Powerful tools: React Toolkit also comes with utilities like RTK Query for data fetching and caching
The Redux team recently released version 2.0 of Redux Toolkit, which added these features:
- A new
combineSlices
method that will lazy load slice reducers for better code splitting - The ability to add middleware at runtime
- Async thunk support in reducers
- The ability to make selectors part of your slice
Further reading:
- Smarter Redux with Redux Toolkit
- Understanding Redux: A tutorial with examples
- Using Redux Toolkit’s
createAsyncThunk
- RTK Query: The future of data fetching and caching for Redux
- Exploring Redux Toolkit 2.0 and the Redux second generation
Why use Redux Toolkit?
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:
- Convenience: Forget writing endless boilerplate for reducers, actions, selectors, and constants. Or tracking down why an action isn't working correctly by searching its name, then the constant, and then finally finding the issue in the reducer — before forgetting the action name and having to backtrack through the code again. RTK's
createSlice
does all the heavy lifting, saving you time and sanity - Performance: Redux Toolkit inherits Redux's single source of truth approach and adds tools like memoized selectors and Immer, which lets you modify state immutably without the overhead of creating a new object for every change
- Ease of use: RTK makes state updates more intuitive and reduces the risk of bugs with Immer. The API is clean, the docs are really all you need to use it, and the learning curve (when compared to vanilla Redux) is gentle. I looked at many options to improve Redux in the early days that would do something similar to RTK, tried a few, and gave up. Since running into Redux Toolkit, I've used it for a couple of years now and am fully on the bandwagon!
- Community and ecosystem: Just because everyone else seems to be using a library doesn't mean you should. However, there’s a large community supporting RTK, a tool that works well. If you have an issue or are struggling with implementing RTK, a Google search will usually turn up others who not only feel your pain but also have already found the solution to your problem
- Documentation: Some libraries are well-documented in that they have a lot of resources available, but in format that is confusing and hard to understand. All I want is some real code examples to show me how to use things, please. However, Redux Toolkit's documentation is both thorough and well-organized, with plenty of examples. The docs will take you from getting started to exploring use cases, parameters, exceptions, and more that I haven’t even needed to check out yet
- Integrations: RTK plays well with others, especially React and TypeScript, and reduces the need to create manual type definitions while giving you type safety
Even so, Redux Toolkit is not a one-size-fits-all solution. Here’s why you might reconsider using RTK in your app:
- Bundle size: RTK adds some more weight to your bundle compared to vanilla Redux. But let's be honest, compared to the overall size of modern apps and the benefits it brings, this is a small price to pay
- Customization: While it removes the overhead, RTK does add an abstraction layer. If you need deep control over everything Redux does, pure Redux may gave you more flexibility. But for most apps, you won't need to get that far into the weeds
- Complexity: While Redux Toolkit is easier than vanilla Redux, it still has a learning curve, but there are many resources and a large community to support you. It can be challenging at first, but once you get the hang of it, you will see its value
- Overkill for simple apps: For some apps, you might not need Redux, Redux Toolkit, or any third-party state management library
Further reading:
- Redux immutable update patterns
- 5 key Redux libraries to improve code reuse
- Using TypeScript with Redux Toolkit
- How to build a type-safe React Redux app
- When (and when not) to use Redux
Key Redux Toolkit features to know
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.
Configuring a Redux store
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;
Creating slices with reducers and actions
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;
Adding thunks to your slices
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
},
},
),
}),
});
Further reading:
Adding selectors to your slices
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();
Using middleware
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 usingcreateAction
for consistency -
immutableCheck
: Warns against mutations in the state object -
thunk
: Enables asynchronous operations and side effects by allowing functions within actions to dispatch other actions or interact with external APIs -
serializableCheck
: Warns if non-serializable values are detected in state
You 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);
}
Debugging with Redux DevTools
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.
Use cases for Redux Toolkit
Redux Toolkit is really versatile and can be used for a wide range of applications. Let’s talk about some places where it shines.
Managing complex application state
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:
- Handling data from multiple APIs
- Caching/prefetching data with RTK Query, which comes with RTK
- Communicating across decoupled components
- Undo/redo functionality
- Shared state logic across platforms
Implementing advanced state workflows
Redux Toolkit comes with helpers for complex workflows. Features like createAsyncThunk
and createEntityAdaptor
will speed up implementing:
- Async data fetching
- Realtime updates
- Optimistic UI updates
An ecommerce app may use these features to update cart quantities optimistically after adding items rather than waiting on API responses.
Migrating existing Redux codebases
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's new build process
Redux Toolkit did have some issues in the past that made it incompatible with modern JavaScript features, including:
- ESM incompatibility: RTK couldn't be loaded correctly in both client and server code simultaneously due to missing
exports
field in thepackage.json
file - Incorrect
.mjs
import: Importing RTK in a.mjs
file failed due to the use ofmodule
but noexports
field - TypeScript
node16
module resolution: RTK didn't work with the new TypeScript module resolution option
The Redux team took action to resolve these limitations in v2.0. Here is what they changed:
-
exports
field inpackage.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 - Build output modernization: No more transpilation — RTK now targets modern ES2020 JavaScript syntax, aligning with current JavaScript standards. Additionally, build artifacts are consolidated under
./dist/
, simplifying the package structure. TypeScript support is also enhanced, with the minimum supported version being v4.7 - Dropping UMD builds: UMD builds, primarily used for direct script tag imports, were removed due to their limited use cases today. A browser-ready ESM build —
dist/$PACKAGE_NAME.browser.mjs
— is still available for script tag loading via Unpkg
For more details on these changes, including the reasons for implementing them, check out my experience modernizing packages to ESM.
Redux Toolkit vs. similar
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 |
Further reading:
- Introduction to MobX with React
- Managing React state with Zustand
- Using Jotai with Next.js to share state across your app
- Comparing TypeScript state management solutions
- Understanding state management in Next.js
- A guide to choosing the right React state management solution
Conclusion
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.
Get set up with LogRocket's modern error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Script Tag:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)