Understanding Redux Toolkit: A Complete Guide to Modern State Management in React

Managing state in modern React applications can quickly become complex as apps grow. Redux Toolkit (RTK) was created to simplify that process. It provides a set of tools and conventions that make writing Redux logic more intuitive, concise, and powerful.

In this post, we’ll go in-depth into Redux Toolkit, exploring its main features — such as createSlice, createAsyncThunk, configureStore, createEntityAdapter, and createSelector — and how to use them together with TypeScript for scalable, type-safe state management.

Why Redux Toolkit?

Traditional Redux required a lot of boilerplate: defining constants, creating action creators, writing switch-case reducers, and setting up middleware manually.
Redux Toolkit solves this by introducing opinionated helpers that:

  • Eliminate repetitive code
  • Provide better developer experience
  • Integrate async logic cleanly
  • Work perfectly with TypeScript

You get structure, consistency, and simplicity — all while retaining the power and predictability of Redux.

Setting Up the Store

The store is the heart of every Redux app. With Redux Toolkit, setting it up is straightforward using configureStore.

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import productsReducer from './productsSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    products: productsReducer,
  },
  devTools: process.env.NODE_ENV !== 'production',
});

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

configureStore automatically includes useful defaults such as:

  • Redux Thunk middleware for handling async logic
  • DevTools integration for debugging
  • Middleware checks for immutability and serializability

This means less setup and more productivity.

Creating Reducers and Actions with createSlice

The createSlice function is one of the core utilities in Redux Toolkit. It allows you to define a reducer and automatically generate action creators for each case.

Here’s an example:

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;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

Instead of writing action creators and switch statements manually, createSlice automatically generates them based on your reducers.
You can now dispatch these actions directly in your components.

Handling Async Logic with createAsyncThunk

Dealing with API calls and asynchronous operations is easy with createAsyncThunk. It wraps your async functions and automatically generates action types for pending, fulfilled, and rejected states.

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

interface UserState {
  data: any;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  data: null,
  loading: false,
  error: null,
};

export const fetchUser = createAsyncThunk('user/fetchUser', async (id: number) => {
  const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return response.data;
});

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message ?? 'Failed to fetch user';
      });
  },
});

export default userSlice.reducer;

This approach simplifies async code, improves readability, and automatically handles action types for different stages of the request.

Normalizing Data with createEntityAdapter

When dealing with large lists of entities like users, posts, or products, createEntityAdapter helps you manage normalized data efficiently. It provides prebuilt reducers and selectors for CRUD operations.

import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

interface Product {
  id: number;
  name: string;
  price: number;
}

export const fetchProducts = createAsyncThunk('products/fetchAll', async () => {
  const response = await axios.get('https://fakestoreapi.com/products');
  return response.data as Product[];
});

const productsAdapter = createEntityAdapter<Product>({
  selectId: (product) => product.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name),
});

const productsSlice = createSlice({
  name: 'products',
  initialState: productsAdapter.getInitialState({ loading: false }),
  reducers: {
    addProduct: productsAdapter.addOne,
    removeProduct: productsAdapter.removeOne,
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        productsAdapter.setAll(state, action.payload);
      });
  },
});

export const { addProduct, removeProduct } = productsSlice.actions;
export const productSelectors = productsAdapter.getSelectors((state: RootState) => state.products);
export default productsSlice.reducer;

Using createEntityAdapter helps maintain consistent and optimized state structures, especially when working with relational or list-based data.

Memoized Selectors with createSelector

createSelector (from Reselect, built into Redux Toolkit) allows you to create memoized selectors that compute derived state efficiently.

import { createSelector } from '@reduxjs/toolkit';
import { RootState } from './store';

export const selectProducts = (state: RootState) => state.products.entities;

export const selectExpensiveProducts = createSelector(
  [selectProducts],
  (products) => Object.values(products).filter((p) => p && p.price > 100)
);

With createSelector, your selectors only recompute when the underlying state changes, improving performance and preventing unnecessary re-renders.

Typed Hooks for TypeScript

To make Redux Toolkit work seamlessly with TypeScript, you can create typed versions of useDispatch and useSelector.

import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Now your components will have full type safety and autocomplete when working with Redux.

Final Thoughts

Redux Toolkit has completely modernized how developers write Redux code. It combines simplicity, structure, and performance, making it the go-to choice for managing global state in React.

With features like:

  • createSlice for easy reducers and actions
  • createAsyncThunk for async logic
  • createEntityAdapter for normalized data
  • createSelector for efficient derived state
  • configureStore for streamlined setup

…Redux Toolkit provides everything you need to manage state in a clean, scalable, and type-safe way.

If you’re still using traditional Redux, it’s worth migrating to Redux Toolkit — it will make your codebase simpler, faster, and easier to maintain.

Share this content:

Hi, my name is Toni Naumoski, and I’m a Senior Frontend Developer with a passion for blending code and design. With years of experience as a Frontend Developer, Web Designer, and Creative Technologist, I specialize in crafting unique, responsive, and detail-oriented websites and web applications that stand out. I bring deep expertise in HTML, CSS, and JavaScript working fluently with modern frameworks like React, Angular, and Vue, as well as animation libraries like GSAP. My creative side thrives in Photoshop and Figma, and I enjoy extending functionality using tools like Express.js and ChatGPT. My work is guided by high integrity, strong communication, a positive attitude, and a commitment to being a reliable collaborator. I take pride in delivering high-quality digital experiences that are both technically solid and visually compelling.