Exploring Redux Toolkit 2.0 and the Redux second generation
Tags: redux, webdev
Published: 2024-01-23 19:23:55

**Written by [Stephan Miller
](https://blog.logrocket.com/author/stephan-miller/)✏️**

State management in web applications is a hot topic. But while React's Context API, MobX, and a handful of other libraries might be great alternatives to Redux, Redux is still king.

Redux has earned its stripes. It's predictable, reliable, and has a huge community of users. But even those of us who use it have to be honest: there used to be a lot of boilerplate to deal with, which added complexity and could make tracing variables through your source code a pain. 

But if you're still dealing with this boilerplate, then you need to catch up. Redux Toolkit has been around since 2019 and is now the standard method of creating Redux apps, streamlining your state management, and reducing the amount of boilerplate code you need to write. And if you are already using Redux Toolkit and RTK Query, Redux Toolkit 2.0 was released to production in November 2023, so it's ready to use.

## Redux Toolkit 2.0 overview and installation

Redux Toolkit 2.0 is the first major version of Redux Toolkit in four years and while it's a big overhaul and there are some breaking changes, [Redux documentation](https://redux.js.org/usage/migrations/migrating-rtk-2) states that "most of the breaking changes should not have an actual effect on end users" and that "many projects can just update the package version with very few code changes." Here is an overview of the changes:

### Modernization

*   **Packaging updates:** The modern ESM build lives in `./dist/` with a CJS build included for compatibility
*   **Deprecations removed:** Several options that were marked as deprecated in the past have been removed, such as the outdated object syntax in slices and reducers

### Better workflow

*   **New `combineSlices` method:** Lazy load slice reducers for improved performance and modularity
*   **Object vs. callback syntax:** Both `createSlice` and `createReducer` now use a cleaner callback syntax instead of the deprecated object approach
*   **Dynamic middleware**: You can now add middleware on the fly

### Dependency changes

*   Updated dependencies, like Reselect and Redux Thunk, which you don't have to install separately
*   Requires TypeScript 4.7 or later for optimal compatibility
*   Requires React Redux 9.0 for React apps, which you have to install separately
*   Requires React 18 if you're using React Redux

## Installing Redux Toolkit

Now that we have an idea of the changes and improvements in this new version of Redux Toolkit, let's look at how to migrate a web app to this new version. If you are still using old school, non-Toolkit Redux, I will point you to other posts along the way that will guide you through migrating to the newer way of using Redux. 

The first step is to install the new version, which is v2.0.2 at the time of writing this article:

```shell
# with npm
npm install @reduxjs/toolkit
# or with yarn
yarn add @reduxjs/toolkit
```

This will bring Redux core 5.0, Reselect 5.0, and Redux Thunk 3.0 along with it. If you are installing this in a React app, the new version of React Redux requires updating to React 18. 

Once you have upgraded React or if you are already running this version, install React Redux 9.0 with one of these commands:

```shell
# with npm
npm install react-redux
# or with yarn
yarn add react-redux
```

## Moving to Redux Toolkit 2.0 and Redux core 5.0

If you are still using vanilla Redux, you should check out this [article on moving to Redux Toolkit.](https://blog.logrocket.com/smarter-redux-redux-toolkit/) This installation won't change that because you can still use vanilla Redux with this version, but who would want to? Redux Toolkit changes the following three files into one file:

```javascript
// Actions
const ADD_TODO = 'ADD_TODO';

function addTodo(text) {
  return { type: ADD_TODO, payload: text };
}

// Reducer
function todoReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    default:
      return state;
  }
}

// Store
import { createStore } from 'redux';

const store = createStore(todoReducer);
```

Here is the resulting file:

```javascript
import { createSlice } from '@reduxjs/toolkit';

const todoSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo(state, action) {
      state.push(action.payload);
    },
  },
});

export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;
```

## Redux ToolKit changes in 2.0

Now let's look at how Redux Toolkit improved in the latest version and what changes have to be made during an upgrade.

### Minor TypeScript changes

Here are some of the simple changes you have to make because of TypeScript compatibility updates:

*   **`UnknownAction` replaces `AnyAction`:** Treat any action's fields as `unknown` unless explicitly checked. Use type guards like `.match()` from Redux Toolkit or the new `isAction` utility to verify action types before accessing fields
*   **`Middleware` action and `next` parameters are also `unknown`:** Use type guards to safely interact with actions within the middleware
*   **`PreloadedState` type is gone:** It has been replaced by a generic in the `Reducer` type

### Callback syntax in `createSlice` is now required

This change applies to both `createSlice.extraReducers` and `createReducer`. Up until this version, you could use either type of syntax. Here is an example of how to make this change. 

This is the code block before making the change. We’re using the object syntax:

```javascript
const mySlice = createSlice({
  // ... other reducers
  extraReducers: {
    [fetchTodos.pending]: (state) => {
      state.status = 'loading';
    },
    [fetchTodos.fulfilled]: (state, action) => {
      state.todos = action.payload;
      state.status = 'idle';
    },
    [fetchTodos.rejected]: (state, action) => {
      state.status = 'error';
    },
  },
});
```

And this is after the change. We’re using the callback syntax:

```javascript
const mySlice = createSlice({
  // ... other reducers
extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.todos = action.payload;
        state.status = 'idle';
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'error';
      });
  },
})
```

### Changes to `configureStore`

According to the Redux docs, `createStore` is now deprecated and `configureStore` should be used instead. However, this has been the case since version 4.2.0, so it is not a new development. They are just reiterating this; `createStore` won't be removed because `configureStore` uses it internally, but it shouldn't be used directly. 

Both `configureStore.middleware` and `configureStore.enhancers` must now be callbacks. Here is an example of these changes:

```javascript
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import { batchedSubscribe } from 'redux-batched-subscribe';

const store = configureStore({
  // other configuration options
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
  // NOT THIS: middleware: (getDefaultMiddleware) => return [myMiddleware],
  enhancers: (getDefaultEnhancers) => getDefaultEnhancers().concat(batchedSubscribe()),
  // NOT THIS: enhancers: (getDefaultEnhancers) => return [myEnhancer],
});
```

The order of `middleware` and `enhancers` matters. For internal type inference to work, `middleware` has to come first. 

You now have to use the `Tuple` type to provide an array of custom middleware or enhancers to `configureStore`. A plain array often leads to type loss, while `Tuple` maintains type safety. Here is an example:

```javascript
import { configureStore, Tuple } from '@reduxjs/toolkit';
import logger from 'redux-logger';

configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    new Tuple(getDefaultMiddleware(), myCustomMiddleware, logger),
});
```

### Changes to customizing `reactHooksModule`

Previously you could introduce your own custom versions of `useSelector`, `useDispatch`, and `useStore` but there was no way to check that all three were added. This module is now under the key of `hooks` and there is a check to determine whether all three exist:

```javascript
// What you could do before
const customCreateApi = buildCreateApi(
  coreModule(),
  reactHooksModule({
    useDispatch: createDispatchHook(MyContext),
    useSelector: createSelectorHook(MyContext),
  })
);

// How you do it now
const customCreateApi = buildCreateApi(
  coreModule(),
  reactHooksModule({
    hooks: {
      useDispatch: createDispatchHook(MyContext),
      useSelector: createSelectorHook(MyContext),
      useStore: createStoreHook(MyContext),
    },
  })
);
```

### New Thunk support in `createSlice.reducers`

Redux Toolkit 2.0 introduces the ability to add async thunks within `createSlice.reducers`. 

To do so, first set up a custom version of `createSlice` using `buildCreateSlice` with access to `createAsyncThunk`. Then, use a callback for `reducers` to define thunks and other reducers. Finally, employ `create.asyncThunk` within the callback. 

Here is an example:

```javascript
const createSliceWithThunks = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
});

const todosSlice = createSliceWithThunks({
  name: 'todos',
  reducers: (create) => ({
    // Normal reducers
    deleteTodo: create.reducer(...),
    // Async thunk
    fetchTodo: create.asyncThunk(
      async (id, thunkApi) => {
        const res = await fetch(`myApi/`);
        return (await res.json());
      },
      {
        pending: (state) => { ... },
        fulfilled: (state, action) => { ... },
        rejected: (state, action) => { ... },
        settled: (state, action) => { ... },
      }
    ),
  }),
});

// Access thunks like regular actions using slice.actions.
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions;
```

### Making selectors part of your slice

You can now define selectors directly within `createSlice`. Here are some points to note:

*   Selectors assume the slice state is mounted at `rootState.{sliceName}`
*   Use `sliceObject.getSelectors(selectSliceState)` to customize selector generation for alternate state locations

Here’s a code example:

```javascript
const mySlice = createSlice({
  name: 'todos',
  reducers: {
    // ... reducers
  },
  selectors: {
    selectTodos: state => state.todos,
    selectTodoById: (state, todoId) => state.todos.find(todo => todo.id === todoId),
  },
});

// Accessing selectors:
const { selectTodos, selectTodoById } = mySlice.selectors;

const todos = selectTodos();
const todo = selectTodoById(42);
```

### Lazy loading and code split slices

Redux Toolkit 2.0 introduces `combineSlices` to enable code splitting and lazy loading reducers. It accepts individual slices or an object of slices and automatically merges them using `combineReducers`. The reducer function it generates provides the following methods:

*   **`inject()`**: Adds slices dynamically, even after the store is created
*   **`withLazyLoadedSlices()`**: Generates TypeScript types for slices to be added later

Here is an example:

```javascript
// Combine slices and add lazy loaded type
import { combineSlices } from '@reduxjs/toolkit';
import slice1 from './slice1';
import slice2 from './slice2';
import lazyLoadedSlice from './lazyLoadedSlice';

const rootReducer = combineSlices(slice1, slice2).withLazyLoadedSlices<
    WithSlice<typeof lazyLoadedSlice>
  >();

// Later, inject new slice lazy loaded slice:
import lazyLoadedSlice from './lazyLoadedSlice';

rootReducer.inject(lazyLoadedSlice);
```

### Dynamically add middleware

It used to take a hack or a separate package to add middleware at runtime, which can be useful for code splitting. Now you can do this with Redux Toolkit 2.0:

```javascript
// Import, create dynamic instance, and configure your store with it.
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'

const dynamicMiddleware = createDynamicMiddleware()

const store = configureStore({
  reducer: {
    myThings: myThingsReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(dynamicMiddleware.middleware),
})

// Add your middleware at runtime
dynamicMiddleware.addMiddleware(loggerMiddleware);

// Add other middleware based on conditions, user input, etc.
if (someCondition) {
  dynamicMiddleware.addMiddleware(otherMiddleware);
}
```

`createDynamicMiddleware` also comes with [React hook integration](https://redux-toolkit.js.org/api/createDynamicMiddleware#react-integration) (if you have React Redux 9.0 installed).

## Reselect 5.0: Changes and new features

Reselect now uses a `WeakMap`-based memoization function called `weakMapMemoize` by default. It offers better performance and memory management compared to the previous `defaultMemoize` function. The cache size is effectively infinite, but it now relies exclusively on reference comparison. 

The older `defaultMemoize` function is now available as `lruMemoize` for those who need a Least Recently Used (LRU) cache. If you want to create custom equality comparisons, you can make `createSelector` use `lruMemoize`. 

You can then pass options to `createSelector` for more control over memoization and debugging:

*   `memoize`: Specifies a custom memoization function (e.g., `lruMemoize`)
*   `argsMemoize`: Customizes memoization behavior for selector arguments
*   `inputStabilityCheck`: Enables a development-time check for input selector stability
*   `identityFunctionCheck`: Warns if the result function returns its input directly

Here is an example of specifying the older `memoize` function instead of the default `weakMapMemoize` along with some of these new options:

```javascript
import { createSelector } from 'reselect';

const mySelector = createSelector(
  state => state.todos,
  todos => todos.filter(todo => todo.completed),
  {
    memoize: lruMemoize, // Use LRU cache, runs the input selectors and compares their current results with the previous ones
    memoizeOptions: { resultEqualityCheck: (a, b) => a === b } // Custom equality comparison
    argsMemoize: defaultMemoize, // Use default memoize function, compares the current arguments with the previous ones
    argsMemoizeOptions: { isEqual: (a, b) => a === b }, // Custom equality comparison for argsMemoize
    inputStabilityCheck: true, // Enable input stability check
  }
);
```

## Changes to RTK Query 2.0

Now, if you aren't using Redux Toolkit, you definitely aren't using RTK Query. I started using Toolkit around two years ago and just happened to run into RTK Query about six months ago when I was looking for a simplified way of fetching data for the dashboard. I wish I had found it earlier! 

While it doesn't replace React Toolkit, [RTK Query is great for fetching data](https://blog.logrocket.com/rtk-query-future-data-fetching-caching-redux/). It will even take out your service files if you currently have them, which means less boilerplate. 

Only a few things were changed in RTK Query 2.0\. The development team stated that the focus for 2.0 was improvements to the core Redux Toolkit libraries and now that they’re done with that, they can shift attention to improving the RTK Query library. But some issues were fixed, including:

*   Some users reported issues with manually skipping subscriptions and running multiple lazy queries. These bugs were due to RTK Query not tracking cache entries in certain scenarios
*   Running multiple mutations consecutively could cause problems with tag invalidation (updating related data based on changes). RTK Query now lets you choose how tag invalidation happens. By default, it waits briefly to group multiple invalidations, preventing unnecessary processing, but if you prefer the old behavior, you can switch it back in the configuration by setting `invalidationBehavior` to `immediate`

## Not much changed with React Redux 9.0

Redux Toolkit 2 requires React Redux 9 in React-based apps. The changes to React Redux were relatively minor, mainly to make it compatible with the other Redux changes.

## Conclusion

Redux Toolkit 2.0 is here, and it is not yesterday's Redux, but it hasn't been for a while. Redux Toolkit and RTK Query have been around for four years now and reduced a lot of the boilerplate, which was the biggest complaint about Redux. 

But this new version adds even more reasons to give it a try, including streamlined, modern packaging and the removal of outdated and deprecated features. Slices can now be lazy loaded and string-based action types simplify debugging. Finally, upgrading doesn't require many code changes and, according to the docs and my experience, won't affect your users.

---

## Get set up with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
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

[Get started now](https://lp.logrocket.com/blg/signup)