Redux Toolkit Overview and Benefits

The Official, Opinionated, Batteries-Included Toolset for Redux

Introduction to Redux Toolkit

Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It was created to address three common concerns about Redux:

flowchart TB Redux[Redux Core] --> |Enhances| RTK["Redux Toolkit (RTK)"] RTK --> ConfigureStore["configureStore() Simplified store setup"] RTK --> CreateReducer["createReducer() Write reducers with Immer"] RTK --> CreateAction["createAction() Simplified action creators"] RTK --> CreateSlice["createSlice() Generate actions & reducers"] RTK --> Thunks["Built-in thunk middleware"] RTK --> Devtools["DevTools Extension"] RTK --> RTKQuery["RTK Query Data fetching & caching"] classDef redux fill:#f99,stroke:#933,stroke-width:2px; classDef rtk fill:#9af,stroke:#36a,stroke-width:2px; classDef api fill:#9f9,stroke:#393,stroke-width:2px; class Redux redux; class RTK rtk; class ConfigureStore,CreateReducer,CreateAction,CreateSlice,Thunks,Devtools,RTKQuery api;

Redux Toolkit packages all the Redux essentials in one place, providing utilities to simplify common Redux use cases.


// Installing Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux

// or
yarn add @reduxjs/toolkit react-redux
        

Real-World Analogy: Home Building Toolkit

Redux Core is like being given the raw materials to build a house—lumber, nails, concrete, and tools. You have complete flexibility, but you need to make many decisions and perform many steps to build even a basic structure.

Redux Toolkit is like being given a premium homebuilder's toolkit that includes:

  • Pre-made wall frames (createSlice)
  • Power tools instead of hand tools (Immer for immutability)
  • Best practice templates and guides (built-in middleware)
  • Construction team with experience (opinionated defaults)

You still control the final design, but much of the repetitive work and decision-making is already handled, allowing you to build faster with fewer mistakes.

Key Features of Redux Toolkit

1. configureStore()

Wraps the Redux createStore function with simplified configuration options and good defaults. It automatically sets up:


// Before (Redux core)
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { usersReducer, postsReducer, commentsReducer } from './reducers';

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer,
  comments: commentsReducer
});

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));

const store = createStore(rootReducer, composedEnhancer);

// After (Redux Toolkit)
import { configureStore } from '@reduxjs/toolkit';
import { usersReducer, postsReducer, commentsReducer } from './reducers';

const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
  // middleware, devTools, preloadedState, and enhancers are all configurable
});
        

2. createAction()

A utility that simplifies creating Redux action creators. It generates an action creator function and automatically uses the function name as the action type.


// Before (Redux core)
const ADD_TODO = 'todos/addTodo';

const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { text, id: nanoid() }
});

// After (Redux Toolkit)
import { createAction } from '@reduxjs/toolkit';
import { nanoid } from '@reduxjs/toolkit';

const addTodo = createAction('todos/addTodo', (text) => ({
  payload: { text, id: nanoid() }
}));

// The action creator's type is available as a property
console.log(addTodo.type); // 'todos/addTodo'

// You can still call it like a regular action creator
const action = addTodo('Write code');
console.log(action);
// { type: 'todos/addTodo', payload: { text: 'Write code', id: '4AJvwMSWEHCchcWYga3dj' } }
        

3. createReducer()

Provides a way to create reducers that use a lookup table approach instead of switch statements. It also uses Immer internally, which allows you to write "mutating" code that is actually handled immutably.


// Before (Redux core)
const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return {
        ...state,
        value: state.value + 1
      };
    case 'counter/decrement':
      return {
        ...state,
        value: state.value - 1
      };
    case 'counter/incrementByAmount':
      return {
        ...state,
        value: state.value + action.payload
      };
    default:
      return state;
  }
}

// After (Redux Toolkit)
import { createReducer } from '@reduxjs/toolkit';

const increment = createAction('counter/increment');
const decrement = createAction('counter/decrement');
const incrementByAmount = createAction('counter/incrementByAmount');

const initialState = { value: 0 };

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state) => {
      // "Mutate" the state directly - Immer makes it immutable behind the scenes
      state.value += 1;
    })
    .addCase(decrement, (state) => {
      state.value -= 1;
    })
    .addCase(incrementByAmount, (state, action) => {
      state.value += action.payload;
    });
});
        

4. createSlice()

The crown jewel of Redux Toolkit—it combines createAction and createReducer to generate action creators, action types, and reducers all in one function call. This drastically reduces boilerplate code.


// Redux Toolkit's createSlice
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

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

// Export the reducer
export default counterSlice.reducer;
        

5. Built-in Thunk Middleware and DevTools Support

Redux Toolkit includes Redux Thunk for handling async logic and automatically sets up the Redux DevTools Extension for debugging.


// Async action with createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return await response.json();
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: {}, loading: 'idle' },
  reducers: {
    // Regular reducers here
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'loading';
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.entities[action.payload.id] = action.payload;
        state.loading = 'idle';
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.error.message;
      });
  }
});
        

6. RTK Query

Introduced in Redux Toolkit 1.6, RTK Query is a powerful data fetching and caching tool that eliminates the need to write data fetching logic by hand.


// RTK Query example
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

// Define the API service
export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com/' }),
  endpoints: (builder) => ({
    // Define an endpoint to fetch posts
    getPosts: builder.query({
      query: () => 'posts'
    }),
    // Define an endpoint to fetch a single post
    getPost: builder.query({
      query: (id) => `posts/${id}`
    }),
    // Define an endpoint to create a post
    createPost: builder.mutation({
      query: (newPost) => ({
        url: 'posts',
        method: 'POST',
        body: newPost
      })
    })
  })
});

// Export generated hooks
export const { useGetPostsQuery, useGetPostQuery, useCreatePostMutation } = api;
        

Comparison: Traditional Redux vs. Redux Toolkit

Task Traditional Redux Redux Toolkit
Setting up store Manual: combineReducers, applyMiddleware, etc. Automatic with configureStore
Creating actions Action type constants + action creators Action creators generated by createSlice
Immutable updates Manual spreading: {...state, value: newValue} "Mutable" code with Immer
Async logic Install thunk + write async action creators createAsyncThunk or RTK Query
DevTools setup Manual configuration Automatic
Entity management Manual normalization createEntityAdapter
Data fetching Custom logic with thunks RTK Query with hooks

Benefits of Using Redux Toolkit

1. Less Boilerplate Code

Perhaps the most significant benefit of Redux Toolkit is the dramatic reduction in boilerplate code. By combining action creators, action types, and reducers into slices, you can write significantly less code.

pie title "Lines of Code Comparison" "Traditional Redux" : 100 "Redux Toolkit" : 40

2. Built-in Best Practices

Redux Toolkit enforces Redux best practices by default:

3. Simplified Immutability

With Immer integration, you can write reducers that appear to mutate state directly, but actually produce immutable updates. This makes your code more readable and less error-prone.


// Without Immer
const todosReducer = (state = [], action) => {
  switch (action.type) {
    case 'todos/todoAdded': {
      return [
        ...state,
        {
          id: action.payload.id,
          text: action.payload.text,
          completed: false
        }
      ];
    }
    case 'todos/todoToggled': {
      return state.map(todo => {
        if (todo.id !== action.payload) {
          return todo;
        }
        return {
          ...todo,
          completed: !todo.completed
        };
      });
    }
    default:
      return state;
  }
};

// With Immer (via Redux Toolkit)
const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    todoAdded: (state, action) => {
      // Can write "mutating" logic because Immer makes it immutable
      state.push({
        id: action.payload.id,
        text: action.payload.text,
        completed: false
      });
    },
    todoToggled: (state, action) => {
      // Find the todo and toggle its completed flag
      const todo = state.find(todo => todo.id === action.payload);
      todo.completed = !todo.completed;
    }
  }
});
        

4. Simplified Async Logic

Redux Toolkit provides createAsyncThunk for handling async operations, which automatically generates pending/fulfilled/rejected action types and dispatches them based on the promise's state.


// Traditional Redux with thunks
export const fetchTodos = () => async dispatch => {
  dispatch({ type: 'todos/fetchTodos/pending' });
  try {
    const response = await fetch('/api/todos');
    const data = await response.json();
    dispatch({ type: 'todos/fetchTodos/fulfilled', payload: data });
  } catch (error) {
    dispatch({ type: 'todos/fetchTodos/rejected', error: error.toString() });
  }
};

// Redux Toolkit with createAsyncThunk
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await fetch('/api/todos');
    return response.json();
  }
);

// Then in your slice:
const todosSlice = createSlice({
  name: 'todos',
  initialState: { entities: [], loading: 'idle' },
  reducers: {
    // Regular reducers here
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.loading = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.loading = 'idle';
        state.entities = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.error.message;
      });
  }
});
        

5. Improved Developer Experience

Redux Toolkit improves the developer experience by providing better error messages, type checking, and integration with development tools.

6. TypeScript Integration

Redux Toolkit is written in TypeScript and provides excellent type definitions, making it easier to use Redux in TypeScript projects.


// TypeScript with Redux Toolkit
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    }
  }
});
        

Real-World Example: E-commerce App

Let's consider how Redux Toolkit simplifies building an e-commerce application:

Features:

  • Product catalog browsing
  • Shopping cart management
  • User authentication
  • Order processing

Implementation with RTK:

  1. Store setup: One configureStore call with all reducers
  2. Product catalog: RTK Query for fetching and caching products
  3. Shopping cart: createSlice for cart management with Immer-powered reducers
  4. User auth: createAsyncThunk for login/registration flows
  5. Order processing: Combined createAsyncThunk and RTK Query for submission and tracking

This approach eliminates hundreds of lines of boilerplate while providing better performance and developer experience.

RTK Query: Data Fetching Revolution

RTK Query is a powerful addition to Redux Toolkit that provides a standardized way to fetch, cache, and update data in your Redux applications.

Key Features

Basic RTK Query Implementation


// src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // Query endpoints (GET requests)
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPost: builder.query({
      query: (id) => `/posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),
    
    // Mutation endpoints (POST, PUT, DELETE requests)
    addPost: builder.mutation({
      query: (post) => ({
        url: '/posts',
        method: 'POST',
        body: post
      }),
      invalidatesTags: ['Post']
    }),
    updatePost: builder.mutation({
      query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
    }),
    deletePost: builder.mutation({
      query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
      }),
      invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
    })
  })
});

// Export the auto-generated hooks
export const {
  useGetPostsQuery,
  useGetPostQuery,
  useAddPostMutation,
  useUpdatePostMutation,
  useDeletePostMutation
} = apiSlice;
        

Using RTK Query Hooks in Components


// src/features/posts/PostsList.jsx
import React from 'react';
import { useGetPostsQuery } from '../api/apiSlice';
import PostItem from './PostItem';

export function PostsList() {
  // The query hook automatically fetches data when the component mounts
  const {
    data: posts = [],
    isLoading,
    isSuccess,
    isError,
    error
  } = useGetPostsQuery();

  let content;

  if (isLoading) {
    content = <div>Loading...</div>;
  } else if (isSuccess) {
    content = posts.map(post => <PostItem key={post.id} post={post} />);
  } else if (isError) {
    content = <div>Error: {error.toString()}</div>;
  }

  return (
    <section>
      <h2>Posts</h2>
      {content}
    </section>
  );
}

// src/features/posts/AddPostForm.jsx
import React, { useState } from 'react';
import { useAddPostMutation } from '../api/apiSlice';

export function AddPostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  
  // The mutation hook returns a trigger function and result object
  const [addPost, { isLoading }] = useAddPostMutation();

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      // Call the mutation trigger function
      await addPost({ title, content }).unwrap();
      // Reset form fields on success
      setTitle('');
      setContent('');
    } catch (err) {
      console.error('Failed to save the post: ', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="Post title"
        required
      />
      <textarea
        value={content}
        onChange={e => setContent(e.target.value)}
        placeholder="Post content"
        required
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Adding...' : 'Add Post'}
      </button>
    </form>
  );
}
        

Why RTK Query Instead of Other Solutions?

RTK Query offers several advantages over other data fetching libraries:

Feature RTK Query React Query Apollo Client Custom Redux/Thunk
Redux Integration Native Separate Separate Native
Caching Automatic Automatic Automatic Manual
Normalized Cache Yes Limited Yes Manual
DevTools Redux DevTools React Query DevTools Apollo DevTools Redux DevTools
Code Generation Automatic hooks No Yes (GraphQL) No
Optimistic Updates Yes Yes Yes Manual
Bundle Size Impact Medium (if already using Redux) Small Large Small

If you're already using Redux, RTK Query provides seamless integration while offering capabilities that would otherwise require multiple packages or complex custom code.

Migration Strategies

If you're already using Redux in your application, you can migrate to Redux Toolkit incrementally:

Incremental Migration Steps

  1. Replace createStore with configureStore
  2. Convert one reducer at a time to use createSlice
  3. Replace thunks with createAsyncThunk
  4. Gradually adopt RTK Query for data fetching

// Step 1: Replace createStore with configureStore
// Before
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

// After
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

const store = configureStore({
  reducer: rootReducer
  // thunk is included by default
  // DevTools extension is enabled by default
});

// Step 2: Convert a reducer to createSlice (one at a time)
// Before
const initialState = { value: 0 };

export function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return { ...state, value: state.value + 1 };
    case 'counter/decrement':
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

export const increment = () => ({ type: 'counter/increment' });
export const decrement = () => ({ type: 'counter/decrement' });

// After
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;
        

Potential Migration Challenges

Practice Activities

Activity 1: Redux Toolkit Store Setup

Create a new Redux application using Redux Toolkit:

  1. Set up a new React project (using Create React App or Vite)
  2. Install Redux Toolkit and React-Redux
  3. Create a Redux store using configureStore
  4. Set up the Provider component in your app
  5. Verify store setup using Redux DevTools

Activity 2: Creating Your First Slice

Implement a counter feature using createSlice:

  1. Create a counterSlice.js file
  2. Define an initial state with a value property
  3. Create increment, decrement, and incrementByAmount reducers
  4. Export the actions and reducer
  5. Add the slice reducer to your store
  6. Create a Counter component that uses the slice
  7. Test your implementation by dispatching actions

Activity 3: Exploring RTK Query

Set up RTK Query to fetch data from an API:

  1. Choose a simple API (like JSONPlaceholder)
  2. Create an API slice using createApi
  3. Define endpoints for fetching data
  4. Add the API slice to your store
  5. Create a component that uses the generated hooks
  6. Display loading states, data, and potential errors
  7. Test caching behavior by mounting/unmounting components

Summary

Further Resources