Stephan Miller

Redux Toolkit adoption guide - Overview, examples, and alternatives

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.

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

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

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;

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
       },
      },
    ),
  }),
});

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;
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 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;
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),
});
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.

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 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 the package.json file
  • Incorrect .mjs import: Importing RTK in a .mjs file failed due to the use of module but no exports 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 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
  • 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.

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:

FeatureRedux ToolkitMobXZustandJotai
Data immutabilityEnforcedOptionalEnforcedEnforced
Time travelAvailable through DevToolsBuilt-inAvailable through Zustand Devtools
Server-side renderingSupportedSupportedSupportedSupported
TypeScript supportExcellentExcellentGoodExcellent
Learning curveModerateEasyEasyEasy

Next, let’s compare them in terms of performance:

LibraryBenchmarksConsiderations
Redux ToolkitGenerally performs well, especially with optimizationsOverhead of createSlice and reducers
MobXCan be faster for simple applications, but complex reactivity can lead to performance issuesRequires careful tracking of dependencies to avoid performance bottlenecks
ZustandCan be lightweight and performant, but may not scale well for large state treesRequires manual configuration for complex state management
JotaiOften praised for its performance, particularly with smaller state slicesLess mature ecosystem and may require more setup for complex use cases

It’s also important to consider the community supporting each library:

LibraryGitHub starsActive contributors
Redux Toolkit10.3k+ (60.3+ for Redux)339+ (979+ for Redux)
MobX27k+315+
Zustand40.3k+223+
Jotai16.7k+178+

Finally, let’s compare their documentation and resources:

LibraryDocumentationTutorials/Examples
Redux ToolkitExtensive and well-maintainedNumerous community resources and tutorials
MobXComprehensive, but slightly less clear than RTKFewer tutorials and community resources
ZustandGood documentation, but less comprehensive than RTKGrowing community resources and tutorials
JotaiConcise and well-written, but limited compared to RTKFewer 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:

FeatureRedux ToolkitMobXZustandJotai
Data immutabilityEnforcedOptionalEnforcedEnforced
Time travelDevToolsBuilt-inDevTools
Server-side renderingSupportedSupportedSupportedSupported
TypeScript supportExcellentExcellentGoodExcellent
Learning curveModerateEasyEasyEasy
PerformanceGood (may require optimization)Can be faster for simple appsLightweight, may not scale wellPerformant, setup required
CommunityLarge and activeSmaller but activeGrowingSmaller but active
DocumentationExtensive and well-maintainedComprehensiveGoodConcise

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.

This article was originally published on LogRocket

Stephan Miller

Written by

Kansas City Software Engineer and Author

Twitter | Github | LinkedIn

Updated