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 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 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:
./dist/
with a CJS build included for compatibilitycombineSlices
method: Lazy load slice reducers for improved performance and modularitycreateSlice
and createReducer
now use a cleaner callback syntax instead of the deprecated object approachNow 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:
# 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:
# with npm npm install react-redux # or with yarn yarn add react-redux
If you are still using vanilla Redux, you should check out this article on moving to 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:
// 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:
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;
Now let’s look at how Redux Toolkit improved in the latest version and what changes have to be made during an upgrade.
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 fieldsMiddleware
action and next
parameters are also unknown
: Use type guards to safely interact with actions within the middlewarePreloadedState
type is gone: It has been replaced by a generic in the Reducer
typecreateSlice
is now requiredThis 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:
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:
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'; }); }, })
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:
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:
import { configureStore, Tuple } from '@reduxjs/toolkit'; import logger from 'redux-logger'; configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => new Tuple(getDefaultMiddleware(), myCustomMiddleware, logger), });
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:
// 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), }, }) );
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:
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;
You can now define selectors directly within createSlice
. Here are some points to note:
rootState.{sliceName}
sliceObject.getSelectors(selectSliceState)
to customize selector generation for alternate state locationsHere’s a code example:
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);
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 createdwithLazyLoadedSlices()
: Generates TypeScript types for slices to be added laterHere is an example:
// 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);
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:
// 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 (if you have React Redux 9.0 installed).
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 argumentsinputStabilityCheck
: Enables a development-time check for input selector stabilityidentityFunctionCheck
: Warns if the result function returns its input directlyHere is an example of specifying the older memoize
function instead of the default weakMapMemoize
along with some of these new options:
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 } );
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. 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:
invalidationBehavior
to immediate
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.
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.
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>
Would you be interested in joining LogRocket's developer community?
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 nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.