React Redux Toolkit

Redux Toolkit (RTK) is the official, recommended way to write Redux. It removes the boilerplate that made classic Redux verbose and error-prone. RTK includes utilities for creating the store, defining reducers with actions, and handling async logic — all with significantly less code than classic Redux.

Why Redux Toolkit

Classic Redux required: separate action type constants, hand-written action creators, manually combining reducers, extra packages for immutability and DevTools. Redux Toolkit replaces all of that with a few simple functions.

Classic Redux (same feature):       Redux Toolkit:
~80 lines of code                   ~25 lines of code
4 separate files                    1 slice file
Manual immutability                 Immer built-in
DevTools needs separate setup       Included by default

Installation

npm install @reduxjs/toolkit react-redux

Core Concepts: createSlice

A "slice" combines a reducer and its action creators in one place. createSlice generates both automatically.

// features/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",           // Name used in action type strings
  initialState: { value: 0 },  // Starting state
  reducers: {
    // Each function here becomes both a reducer case AND an action creator
    increment: (state) => {
      state.value += 1;  // Looks like mutation but Immer handles immutability
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
    },
  },
});

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

// Export reducer for the store
export default counterSlice.reducer;

Setting Up the Store: configureStore

// app/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counterSlice";
import cartReducer from "../features/cartSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    cart: cartReducer,
  },
  // Redux DevTools Extension is enabled automatically in development
});
// main.jsx
import { Provider } from "react-redux";
import { store } from "./app/store";

ReactDOM.createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <App />
  </Provider>
);

Using State in Components

import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementByAmount, reset } from "./counterSlice";

function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
    </div>
  );
}

Real-World Example: Shopping Cart Slice

// features/cartSlice.js
import { createSlice } from "@reduxjs/toolkit";

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    items: [],
    totalQuantity: 0,
    totalPrice: 0,
  },
  reducers: {
    addItem: (state, action) => {
      const existing = state.items.find(item => item.id === action.payload.id);
      if (existing) {
        existing.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      state.totalQuantity += 1;
      state.totalPrice += action.payload.price;
    },
    removeItem: (state, action) => {
      const item = state.items.find(i => i.id === action.payload);
      if (item) {
        state.totalQuantity -= item.quantity;
        state.totalPrice -= item.price * item.quantity;
        state.items = state.items.filter(i => i.id !== action.payload);
      }
    },
    clearCart: (state) => {
      state.items = [];
      state.totalQuantity = 0;
      state.totalPrice = 0;
    },
  },
});

export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;

Async Operations: createAsyncThunk

For API calls, use createAsyncThunk. It automatically handles pending, fulfilled, and rejected states.

// features/usersSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

// Thunk: async operation that dispatches actions automatically
export const fetchUsers = createAsyncThunk("users/fetchAll", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  return response.json(); // Returned value becomes action.payload
});

const usersSlice = createSlice({
  name: "users",
  initialState: { list: [], loading: false, error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.list = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export default usersSlice.reducer;
// Component using the async thunk
function UserList() {
  const dispatch = useDispatch();
  const { list, loading, error } = useSelector((state) => state.users);

  useEffect(() => {
    dispatch(fetchUsers()); // Dispatch the async thunk
  }, [dispatch]);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {list.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

RTK Query: Data Fetching Built Into RTK

RTK Query is an advanced data fetching tool included in Redux Toolkit. It handles caching, background updates, and loading states automatically.

// services/api.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com/" }),
  endpoints: (builder) => ({
    getPosts: builder.query({ query: () => "posts" }),
    getPost: builder.query({ query: (id) => `posts/${id}` }),
  }),
});

export const { useGetPostsQuery, useGetPostQuery } = postsApi;
// Component
function PostList() {
  const { data: posts, isLoading, error } = useGetPostsQuery();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading posts</p>;

  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

Recommended File Structure

src/
  app/
    store.js           ← configureStore()
  features/
    counter/
      counterSlice.js  ← createSlice()
      Counter.jsx      ← Component
    cart/
      cartSlice.js
      Cart.jsx
    users/
      usersSlice.js
      UserList.jsx

Summary

Redux Toolkit modernizes Redux with far less code and better defaults. createSlice generates both reducers and action creators from one definition. configureStore combines reducers and enables DevTools automatically. Writing state.value += 1 looks like mutation but Immer inside RTK handles immutability. Use createAsyncThunk for API calls — it dispatches pending, fulfilled, and rejected states automatically. RTK Query takes data fetching further with automatic caching, deduplication, and background refresh. Organize code by feature, with each feature owning its slice and components together.

Leave a Comment

Your email address will not be published. Required fields are marked *