Share
1

Why Redux Toolkit? Simplifying Redux Development

by ObserverPoint · July 9, 2025

You’ve delved into the core concepts of Redux, understanding its principles and how to manually set up a store, combine reducers, dispatch actions, and subscribe to state changes. While this foundational knowledge is crucial, raw Redux can sometimes feel verbose and require a lot of boilerplate code. This is where Redux Toolkit (RTK) comes in. Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It simplifies common Redux tasks, reduces boilerplate, and makes it easier to write good Redux code.

Redux Toolkit is highly recommended for all new Redux applications, and even for refactoring existing ones. It abstracts away much of the manual setup and encourages best practices, significantly improving developer experience and code maintainability, especially in complex React applications that integrate with Redux for state management.


Why Redux Toolkit? Simplifying Redux Setup

Before Redux Toolkit, setting up a Redux store often involved multiple steps and external libraries for common patterns (like `redux-thunk` for async operations, `reselect` for memoized selectors). This could lead to a lot of boilerplate code and make it daunting for newcomers. Redux Toolkit addresses these pain points directly.

Problems with Traditional Redux Setup:

  • Too Much Boilerplate: Defining action types, action creators, and reducers separately for each feature could generate a significant amount of repetitive code.
  • Complexity in Configuration: Setting up the Redux store with middleware, DevTools, and other enhancers required specific knowledge and careful configuration.
  • Immutability Challenges: Ensuring immutability in reducers manually (by always returning new state objects) could be tricky and error-prone.
  • Async Logic Complexity: Handling asynchronous operations (e.g., API calls) often required additional middleware like `redux-thunk` or `redux-saga`, adding another layer of complexity.

How Redux Toolkit Simplifies Things:

  • Opinionated Defaults: Provides sensible defaults for store setup, including Redux DevTools integration and `redux-thunk` middleware out-of-the-box.
  • Reduced Boilerplate: Offers utilities like `createSlice` that automatically generate action creators and action types alongside reducers, drastically cutting down on code.
  • Immutability Made Easy: Uses the Immer library internally, allowing you to write “mutating” logic inside reducers while still producing immutable updates behind the scenes. This makes reducer logic much simpler and less error-prone.
  • Streamlined Async Logic: Introduces `createAsyncThunk` for a standardized and simpler way to handle asynchronous actions and lifecycle, reducing the need for complex custom middleware setup.

In essence, Redux Toolkit is designed to be the standard way to write Redux logic. It wraps around core Redux, providing a higher-level abstraction that makes development faster and more enjoyable, while still retaining all the benefits of the Redux predictable state container.


`configureStore`: Setting Up the Store

The `configureStore` function from Redux Toolkit is the recommended way to create your Redux store. It’s a simplified wrapper around the original `createStore` that automatically handles common setup concerns, such as:

  • Combining your slice reducers.
  • Adding the `redux-thunk` middleware (for handling async logic).
  • Setting up Redux DevTools Extension integration.
  • Enabling development-only checks for immutability and serializability.

You simply pass an object with a `reducer` property (which can be a single reducer or an object of slice reducers) and `configureStore` handles the rest.

import { configureStore } from '@reduxjs/toolkit';

// Assume you have defined your slice reducers (e.g., from createSlice)
// import counterReducer from './features/counter/counterSlice';
// import userReducer from './features/user/userSlice';

// For demonstration, let's define a dummy reducer here
function dummyCounterReducer(state = { value: 0 }, action: { type: string }) {
    switch (action.type) {
        case 'dummy/increment':
            return { ...state, value: state.value + 1 };
        case 'dummy/decrement':
            return { ...state, value: state.value - 1 };
        default:
            return state;
    }
}

const store = configureStore({
    reducer: {
        // Each key here will be a slice of your state
        counter: dummyCounterReducer, // Example: state.counter will be managed by dummyCounterReducer
        // user: userReducer, // If you had a user slice
        // products: productsReducer, // If you had a products slice
    },
    // Middleware and DevTools are automatically set up by default!
    // You can customize them if needed, e.g.,
    // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myCustomMiddleware),
    // devTools: process.env.NODE_ENV !== 'production', // true by default in dev
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

// console.log("Initial RTK store state:", store.getState());
// store.dispatch({ type: 'dummy/increment' });
// console.log("State after dispatch:", store.getState());
    

The `RootState` and `AppDispatch` types are crucial for full TypeScript support in your React-Redux application, enabling strongly typed selectors and dispatch calls.


`createSlice`: Creating Actions and Reducers Simultaneously

The `createSlice` function is the core of Redux Toolkit. It simplifies the process of creating Redux reducers and actions by allowing you to define them together in a single place. It automatically generates action creators and action types based on the reducer’s name and the names of the reducer functions you provide.

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
    value: number;
}

const initialState: CounterState = {
    value: 0,
};

const counterSlice = createSlice({
    name: 'counter', // This will be used as the prefix for action types (e.g., 'counter/increment')
    initialState,
    reducers: {
        // 'increment' becomes an action creator: counterSlice.actions.increment()
        // And its action type is 'counter/increment'
        increment: (state) => {
            // Immer allows us to "mutate" the state directly
            // Behind the scenes, a new immutable state is produced
            state.value += 1;
        },
        // 'decrement' becomes an action creator: counterSlice.actions.decrement()
        decrement: (state) => {
            state.value -= 1;
        },
        // 'incrementByAmount' takes a 'payload'
        // The type of 'action.payload' is automatically inferred as number due to PayloadAction
        incrementByAmount: (state, action: PayloadAction<number>) => {
            state.value += action.payload;
        },
        // 'reset' without payload
        reset: (state) => {
            state.value = 0;
        },
    },
});

// Export the generated action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;

// Export the reducer function
export default counterSlice.reducer;

// --- How you would use it in your store: ---
// import { configureStore } from '@reduxjs/toolkit';
// import counterReducer from './features/counter/counterSlice'; // This file

// const store = configureStore({
//     reducer: {
//         counter: counterReducer,
//     },
// });

// --- How you would dispatch actions: ---
// store.dispatch(increment());
// store.dispatch(incrementByAmount(5));
// console.log(store.getState().counter.value);
    

Key benefits of `createSlice`:

  • Less Code: No need to define separate action types and action creators.
  • Immutability with Ease: Thanks to Immer, you can write “mutating” logic inside reducers, and Redux Toolkit handles the immutable updates for you. This makes reducers much more readable and less error-prone.
  • Type Safety: Integrates seamlessly with TypeScript, providing strong type inference for action creators and payloads.
  • Convention over Configuration: Follows best practices by default.

`createSlice` is the workhorse of Redux Toolkit, making it much faster and safer to define your Redux state logic.


`createAsyncThunk`: Handling Asynchronous Logic

Many React applications need to perform asynchronous operations, such as fetching data from an API. In traditional Redux, this often involved `redux-thunk` middleware and manually dispatching multiple actions (e.g., `REQUEST`, `SUCCESS`, `FAILURE`). Redux Toolkit’s `createAsyncThunk` simplifies this pattern significantly.

`createAsyncThunk` takes an action type string prefix and a payload creator callback function. The payload creator function should return a Promise. `createAsyncThunk` then automatically dispatches lifecycle actions (`pending`, `fulfilled`, `rejected`) based on the Promise’s status, making it easy to manage loading states, success, and errors in your reducers.

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
// Assuming axios is installed: npm install axios
import axios from 'axios';

interface Post {
    userId: number;
    id: number;
    title: string;
    body: string;
}

interface PostsState {
    posts: Post[];
    status: 'idle' | 'loading' | 'succeeded' | 'failed';
    error: string | null;
}

const initialState: PostsState = {
    posts: [],
    status: 'idle',
    error: null,
};

// Define an async thunk for fetching posts
export const fetchPosts = createAsyncThunk(
    'posts/fetchPosts', // Action type prefix (e.g., 'posts/fetchPosts/pending')
    async () => {
        const response = await axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts');
        return response.data; // This data will be the 'payload' of the 'fulfilled' action
    }
);

const postsSlice = createSlice({
    name: 'posts',
    initialState,
    reducers: {
        // Standard, synchronous reducers can go here
        // For example, to clear posts after fetching
        clearPosts: (state) => {
            state.posts = [];
            state.status = 'idle';
            state.error = null;
        }
    },
    // `extraReducers` allows createSlice to respond to other action types
    // not defined in its `reducers` field, like those generated by createAsyncThunk.
    extraReducers: (builder) => {
        builder
            // Handle the 'pending' state
            .addCase(fetchPosts.pending, (state) => {
                state.status = 'loading';
            })
            // Handle the 'fulfilled' state
            .addCase(fetchPosts.fulfilled, (state, action: PayloadAction<Post[]>) => {
                state.status = 'succeeded';
                state.posts = action.payload; // Set posts with the fetched data
            })
            // Handle the 'rejected' state
            .addCase(fetchPosts.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.error.message || 'Something went wrong';
            });
    },
});

export const { clearPosts } = postsSlice.actions; // Exporting any synchronous actions
export default postsSlice.reducer; // Exporting the slice reducer

// --- How you would use it in your React component with react-redux: ---
// import React, { useEffect } from 'react';
// import { useSelector, useDispatch } from 'react-redux';
// import { RootState, AppDispatch } from './app/store'; // Assuming store.ts exports these types
// import { fetchPosts, clearPosts } from './features/posts/postsSlice'; // This file

// const PostsList: React.FC = () => {
//     const dispatch: AppDispatch = useDispatch();
//     const posts = useSelector((state: RootState) => state.posts.posts);
//     const postStatus = useSelector((state: RootState) => state.posts.status);
//     const error = useSelector((state: RootState) => state.posts.error);

//     useEffect(() => {
//         if (postStatus === 'idle') {
//             dispatch(fetchPosts()); // Dispatch the async thunk
//         }
//     }, [postStatus, dispatch]);

//     if (postStatus === 'loading') return <div>Loading posts...</div>;
//     if (postStatus === 'failed') return <div>Error: {error}</div>;

//     return (
//         <div>
//             <h2>Posts</h2>
//             <button onClick={() => dispatch(clearPosts())}>Clear Posts</button>
//             <ul>
//                 {posts.map(post => (
//                     <li key={post.id}>{post.title}</li>
//                 ))}
//             </ul>
//         </div>
//     );
// };
    

`createAsyncThunk` significantly simplifies asynchronous data fetching and state management patterns in Redux, integrating seamlessly with `createSlice` to handle the different lifecycle stages of a network request (pending, fulfilled, rejected) with minimal boilerplate and maximum type safety. This is a game-changer for building robust React applications that interact with APIs.


References

You may also like