Redux toolkit implementation deep dive

Engineering Manager
July 24, 2024
Updated on August 28, 2024
0 MIN READ
#performance#typescript#web3#microservices#redux

Introduction

Redux Toolkit (RTK) has become the standard way to write Redux logic, offering a more efficient and developer-friendly approach compared to traditional Redux. It provides utilities to simplify common Redux use cases, reduces boilerplate code, and includes best practices by default. In this deep dive, we'll explore the core concepts of Redux Toolkit, its key features, and how to implement it effectively in a React application.

Whether you're new to Redux or looking to modernize your existing Redux codebase, this guide will help you understand the practical implementation of Redux Toolkit while highlighting its advantages.


1. Core Concepts of Redux Toolkit

Redux Toolkit is built on three main pillars:

1.1 createSlice for Reducer Logic

The createSlice function automatically generates action creators and action types based on the reducer functions you provide. It uses Immer internally, allowing you to write "mutating" logic in reducers while producing immutable updates.

Here’s an example of a counter slice:

import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment(state) { state.value += 1; }, decrement(state) { state.value -= 1; }, }, }); export const { increment, decrement } = counterSlice.actions; export default counterSlice.reducer;

1.2 configureStore for Store Setup

configureStore simplifies store configuration by providing sensible defaults (like Redux Thunk middleware and DevTools integration) while allowing customization.

import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; const store = configureStore({ reducer: { counter: counterReducer, }, }); export default store;

1.3 createAsyncThunk for Async Logic

Handling async operations (like API calls) is streamlined with createAsyncThunk, which dispatches pending/fulfilled/rejected actions automatically.

import { createAsyncThunk } from '@reduxjs/toolkit'; export const fetchUserData = createAsyncThunk( 'users/fetchById', async (userId) => { const response = await fetch(`/api/users/${userId}`); return response.json(); } );

2. Advanced Patterns with Redux Toolkit

2.1 Combining Slices

For larger applications, you can split your state into multiple slices and combine them in the store.

import { combineReducers } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; import userReducer from './userSlice'; const rootReducer = combineReducers({ counter: counterReducer, user: userReducer, }); export default rootReducer;

2.2 Using createEntityAdapter for Normalized Data

createEntityAdapter helps manage normalized state (e.g., for lists of items) by providing pre-built reducers and selectors.

import { createEntityAdapter } from '@reduxjs/toolkit'; const usersAdapter = createEntityAdapter(); const initialState = usersAdapter.getInitialState(); const usersSlice = createSlice({ name: 'users', initialState, reducers: { addUser: usersAdapter.addOne, updateUser: usersAdapter.updateOne, removeUser: usersAdapter.removeOne, }, }); export const { addUser, updateUser, removeUser } = usersSlice.actions; export default usersSlice.reducer;

2.3 Middleware Customization

While configureStore includes default middleware, you can extend or modify them as needed.

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import logger from 'redux-logger'; const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), });

3. Integrating Redux Toolkit with React

3.1 Setting Up the Provider

Wrap your app with the Redux Provider to make the store available globally.

import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import store from './store'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );

3.2 Using useSelector and useDispatch

Hooks like useSelector and useDispatch provide a modern way to interact with Redux in functional components.

import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement } from './counterSlice'; function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> </div> ); } export default Counter;

3.3 Handling Async Actions

Use createAsyncThunk with extra reducers to manage loading and error states.

const userSlice = createSlice({ name: 'user', initialState: { data: null, status: 'idle', error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUserData.pending, (state) => { state.status = 'loading'; }) .addCase(fetchUserData.fulfilled, (state, action) => { state.status = 'succeeded'; state.data = action.payload; }) .addCase(fetchUserData.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, });

Conclusion

Redux Toolkit significantly improves the Redux development experience by reducing boilerplate, enforcing best practices, and providing powerful utilities like createSlice, configureStore, and createAsyncThunk. By adopting RTK, teams can write cleaner, more maintainable Redux code while leveraging modern patterns like Immer-based state updates and normalized data management.

If you're starting a new project or refactoring an existing Redux codebase, Redux Toolkit is the recommended approach. Its simplicity and efficiency make it an indispensable tool for state management in React applications.

For further learning, explore the official Redux Toolkit documentation and experiment with integrating it into your projects!

Share this article