Learn Redux in 10 DaysDay 2: Redux Toolkit Basics
Chapter 2Learn Redux in 10 Days

Day 2: Redux Toolkit Basics

What You'll Learn Today

  • How to install Redux Toolkit and React-Redux
  • Creating a Store with configureStore
  • Defining state, reducers, and actions with createSlice
  • Building a counter app step by step
  • Building a todo list app
  • Using Provider, useSelector, and useDispatch
  • How Immer enables immutable updates
  • Comparing legacy Redux boilerplate vs RTK

Installation

To use Redux Toolkit, you need two packages.

npm install @reduxjs/toolkit react-redux
Package Purpose
@reduxjs/toolkit Redux core + utilities (createSlice, configureStore, etc.)
react-redux React-Redux bindings (Provider, useSelector, useDispatch, etc.)

configureStore: Creating the Store

configureStore is the function for creating a Redux Store. It replaces legacy Redux's createStore and automatically configures:

  • Redux DevTools Extension integration
  • redux-thunk middleware (for async logic)
  • Development checks (mutation detection, non-serializable value detection)
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    // Register slice reducers here
  }
});

export default store;
TypeScript version
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    // Register slice reducers here
  }
});

// Infer types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

createSlice: Defining a Slice

createSlice is the core API of Redux Toolkit. A single function call defines all of the following:

  • Initial state (initialState)
  • Reducer functions (state update logic)
  • Action Creators (auto-generated)
  • Action Types (auto-generated)
flowchart TB
    subgraph createSlice["createSlice()"]
        Name["name: 'counter'"]
        Initial["initialState: { value: 0 }"]
        Reducers["reducers: { increment, decrement, ... }"]
    end

    createSlice --> Actions["Auto-generated Actions<br/>counter/increment<br/>counter/decrement"]
    createSlice --> Reducer["Exported Reducer<br/>counterSlice.reducer"]

    Actions --> Dispatch["Used with dispatch()"]
    Reducer --> Store["Registered in configureStore"]

    style createSlice fill:#3b82f6,color:#fff
    style Actions fill:#f59e0b,color:#fff
    style Reducer fill:#22c55e,color:#fff

Practice 1: Counter App

Let's build the simplest possible Redux application -- a counter.

Step 1: Create the Slice

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment(state) {
      // Thanks to Immer, you can write "mutating" code
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    },
    incrementByAmount(state, action) {
      state.value += action.payload;
    },
    reset(state) {
      state.value = 0;
    }
  }
});

// Export Action Creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;

// Export Reducer
export default counterSlice.reducer;
TypeScript version
// src/features/counter/counterSlice.ts
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;
    },
    reset(state) {
      state.value = 0;
    }
  }
});

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

Step 2: Configure the Store

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

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

export default store;
TypeScript version
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

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

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

export default store;

Step 3: Wrap the App with Provider

The Provider component makes the Redux Store available to the entire React component tree.

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';

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

Step 4: Access the Store from Components

// src/features/counter/Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from './counterSlice';

function Counter() {
  // useSelector: Read state from the Store
  const count = useSelector((state) => state.counter.value);

  // useDispatch: Get the dispatch function
  const dispatch = useDispatch();

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

export default Counter;
TypeScript version
// src/features/counter/Counter.tsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from './counterSlice';
import type { RootState } from '../../app/store';

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

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

export default Counter;

Verifying the Data Flow

Let's trace what happens when you click the "+1" button.

sequenceDiagram
    participant User as User
    participant UI as Counter Component
    participant Dispatch as dispatch()
    participant Reducer as counterSlice.reducer
    participant Store as Redux Store

    User->>UI: Clicks "+1" button
    UI->>Dispatch: dispatch(increment())
    Note over Dispatch: Action: { type: 'counter/increment' }
    Dispatch->>Reducer: Process Action
    Reducer->>Store: state.value = 0 + 1 = 1
    Store->>UI: useSelector receives new value
    UI->>User: Displays "Counter: 1"

useSelector in Depth

useSelector is the hook for reading state from the Redux Store.

Basic Usage

// Select only the part of state you need from the entire Store
const count = useSelector((state) => state.counter.value);

Key Point: Performance Optimization

useSelector uses reference equality to determine whether to re-render the component. If the selector returns the same reference as last time, the re-render is skipped.

// GOOD: Returning a primitive value (won't re-render if value is the same)
const count = useSelector((state) => state.counter.value);
const userName = useSelector((state) => state.user.name);

// CAUTION: Returning a new object causes re-render every time
// (because a new object reference is created each time)
const userData = useSelector((state) => ({
  name: state.user.name,
  email: state.user.email,
}));
// ↑ This triggers a re-render every time!

// GOOD: Use separate selectors when you need multiple values
const name = useSelector((state) => state.user.name);
const email = useSelector((state) => state.user.email);

useDispatch in Depth

useDispatch returns the dispatch function for sending Actions to the Store.

const dispatch = useDispatch();

// Dispatch the return value of an Action Creator
dispatch(increment());
// This is equivalent to:
// dispatch({ type: 'counter/increment' })

dispatch(incrementByAmount(5));
// This is equivalent to:
// dispatch({ type: 'counter/incrementByAmount', payload: 5 })

Practice 2: Todo List App

Let's build a more practical example -- a todo list application.

Step 1: Create the Todo Slice

// src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';

let nextId = 1;

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    filter: 'all' // 'all' | 'active' | 'completed'
  },
  reducers: {
    addTodo(state, action) {
      state.items.push({
        id: nextId++,
        text: action.payload,
        completed: false,
        createdAt: new Date().toISOString()
      });
    },
    toggleTodo(state, action) {
      const todo = state.items.find(item => item.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodo(state, action) {
      state.items = state.items.filter(item => item.id !== action.payload);
    },
    editTodo(state, action) {
      const { id, text } = action.payload;
      const todo = state.items.find(item => item.id === id);
      if (todo) {
        todo.text = text;
      }
    },
    setFilter(state, action) {
      state.filter = action.payload;
    },
    clearCompleted(state) {
      state.items = state.items.filter(item => !item.completed);
    },
    toggleAll(state) {
      const allCompleted = state.items.every(item => item.completed);
      state.items.forEach(item => {
        item.completed = !allCompleted;
      });
    }
  }
});

export const {
  addTodo,
  toggleTodo,
  deleteTodo,
  editTodo,
  setFilter,
  clearCompleted,
  toggleAll
} = todosSlice.actions;

export default todosSlice.reducer;
TypeScript version
// src/features/todos/todosSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
  createdAt: string;
}

type FilterType = 'all' | 'active' | 'completed';

interface TodosState {
  items: Todo[];
  filter: FilterType;
}

const initialState: TodosState = {
  items: [],
  filter: 'all'
};

let nextId = 1;

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo(state, action: PayloadAction<string>) {
      state.items.push({
        id: nextId++,
        text: action.payload,
        completed: false,
        createdAt: new Date().toISOString()
      });
    },
    toggleTodo(state, action: PayloadAction<number>) {
      const todo = state.items.find(item => item.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodo(state, action: PayloadAction<number>) {
      state.items = state.items.filter(item => item.id !== action.payload);
    },
    editTodo(state, action: PayloadAction<{ id: number; text: string }>) {
      const { id, text } = action.payload;
      const todo = state.items.find(item => item.id === id);
      if (todo) {
        todo.text = text;
      }
    },
    setFilter(state, action: PayloadAction<FilterType>) {
      state.filter = action.payload;
    },
    clearCompleted(state) {
      state.items = state.items.filter(item => !item.completed);
    },
    toggleAll(state) {
      const allCompleted = state.items.every(item => item.completed);
      state.items.forEach(item => {
        item.completed = !allCompleted;
      });
    }
  }
});

export const {
  addTodo,
  toggleTodo,
  deleteTodo,
  editTodo,
  setFilter,
  clearCompleted,
  toggleAll
} = todosSlice.actions;

export default todosSlice.reducer;

Step 2: Register in the Store

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';

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

export default store;
TypeScript version
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';

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

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

export default store;

Step 3: Add Todo Input Component

// src/features/todos/AddTodo.js
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice';

function AddTodo() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch(addTodo(text.trim()));
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Enter a task..."
      />
      <button type="submit">Add</button>
    </form>
  );
}

export default AddTodo;
TypeScript version
// src/features/todos/AddTodo.tsx
import { useState, FormEvent } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice';

function AddTodo() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch(addTodo(text.trim()));
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Enter a task..."
      />
      <button type="submit">Add</button>
    </form>
  );
}

export default AddTodo;

Step 4: Todo List Display Component

// src/features/todos/TodoList.js
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo, deleteTodo, setFilter, clearCompleted, toggleAll } from './todosSlice';

function TodoList() {
  const { items, filter } = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  // Apply filter
  const filteredItems = items.filter(item => {
    if (filter === 'active') return !item.completed;
    if (filter === 'completed') return item.completed;
    return true;
  });

  const activeCount = items.filter(item => !item.completed).length;
  const completedCount = items.filter(item => item.completed).length;

  return (
    <div>
      {/* Filter buttons */}
      <div>
        <button
          onClick={() => dispatch(setFilter('all'))}
          style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
        >
          All ({items.length})
        </button>
        <button
          onClick={() => dispatch(setFilter('active'))}
          style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
        >
          Active ({activeCount})
        </button>
        <button
          onClick={() => dispatch(setFilter('completed'))}
          style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
        >
          Completed ({completedCount})
        </button>
      </div>

      {/* Bulk actions */}
      <div>
        <button onClick={() => dispatch(toggleAll())}>Toggle All</button>
        <button onClick={() => dispatch(clearCompleted())}>Clear Completed</button>
      </div>

      {/* Todo list */}
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>
            <input
              type="checkbox"
              checked={item.completed}
              onChange={() => dispatch(toggleTodo(item.id))}
            />
            <span style={{
              textDecoration: item.completed ? 'line-through' : 'none',
              color: item.completed ? '#999' : '#000'
            }}>
              {item.text}
            </span>
            <button onClick={() => dispatch(deleteTodo(item.id))}>Delete</button>
          </li>
        ))}
      </ul>

      {filteredItems.length === 0 && (
        <p>No tasks found</p>
      )}
    </div>
  );
}

export default TodoList;
TypeScript version
// src/features/todos/TodoList.tsx
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo, deleteTodo, setFilter, clearCompleted, toggleAll } from './todosSlice';
import type { RootState } from '../../app/store';

function TodoList() {
  const { items, filter } = useSelector((state: RootState) => state.todos);
  const dispatch = useDispatch();

  const filteredItems = items.filter(item => {
    if (filter === 'active') return !item.completed;
    if (filter === 'completed') return item.completed;
    return true;
  });

  const activeCount = items.filter(item => !item.completed).length;
  const completedCount = items.filter(item => item.completed).length;

  return (
    <div>
      <div>
        <button
          onClick={() => dispatch(setFilter('all'))}
          style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
        >
          All ({items.length})
        </button>
        <button
          onClick={() => dispatch(setFilter('active'))}
          style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
        >
          Active ({activeCount})
        </button>
        <button
          onClick={() => dispatch(setFilter('completed'))}
          style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
        >
          Completed ({completedCount})
        </button>
      </div>

      <div>
        <button onClick={() => dispatch(toggleAll())}>Toggle All</button>
        <button onClick={() => dispatch(clearCompleted())}>Clear Completed</button>
      </div>

      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>
            <input
              type="checkbox"
              checked={item.completed}
              onChange={() => dispatch(toggleTodo(item.id))}
            />
            <span style={{
              textDecoration: item.completed ? 'line-through' : 'none',
              color: item.completed ? '#999' : '#000'
            }}>
              {item.text}
            </span>
            <button onClick={() => dispatch(deleteTodo(item.id))}>Delete</button>
          </li>
        ))}
      </ul>

      {filteredItems.length === 0 && (
        <p>No tasks found</p>
      )}
    </div>
  );
}

export default TodoList;

Step 5: Main App Component

// src/App.js
import AddTodo from './features/todos/AddTodo';
import TodoList from './features/todos/TodoList';

function App() {
  return (
    <div>
      <h1>Todo List</h1>
      <AddTodo />
      <TodoList />
    </div>
  );
}

export default App;

How Immer Works

The Immer library used internally by Redux Toolkit dramatically improves the Redux development experience.

Why Immer Matters

Redux requires immutable state updates. In legacy Redux, you had to manually write immutable updates using spread syntax.

// Legacy Redux: Manual immutable updates (nightmare with deep nesting)
function reducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        third: {
          ...state.first.second.third,
          value: action.payload
        }
      }
    }
  };
}

// Redux Toolkit (Immer): Intuitive "mutating" syntax
function reducer(state, action) {
  state.first.second.third.value = action.payload;
}

How Immer Works Under the Hood

flowchart LR
    Original["Original State<br/>(Frozen Object)"]
    Draft["Draft<br/>(Proxy Object)"]
    New["New State<br/>(New Frozen Object)"]

    Original -->|"Immer creates a Proxy"| Draft
    Draft -->|"Records changes"| Draft
    Draft -->|"Applies changes to<br/>produce new object"| New

    style Original fill:#3b82f6,color:#fff
    style Draft fill:#f59e0b,color:#fff
    style New fill:#22c55e,color:#fff
  1. Immer creates a Proxy (Draft) of the original state
  2. Your reducer code "mutates" the Draft
  3. Immer tracks all changes and produces a new object with only the modified parts
  4. The original state is never actually changed

Immer Gotchas

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    // OK: Mutate state directly (Immer handles it)
    addTodo(state, action) {
      state.push(action.payload);
    },

    // OK: Return a new value (same as a regular reducer)
    clearAll() {
      return [];
    },

    // BAD: Mutating state AND returning it (will cause an error)
    // badReducer(state, action) {
    //   state.push(action.payload);
    //   return state; // This is NOT allowed!
    // }
  }
});

Rule: Inside a reducer, either mutate the state OR return a new value -- never do both. Doing both will cause an error.


The PayloadAction Type

When using TypeScript, the PayloadAction type lets you type-check the Action payload.

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

interface UserState {
  name: string;
  email: string;
  age: number;
}

const userSlice = createSlice({
  name: 'user',
  initialState: { name: '', email: '', age: 0 } as UserState,
  reducers: {
    // PayloadAction<T> specifies the payload type
    setName(state, action: PayloadAction<string>) {
      state.name = action.payload; // action.payload is typed as string
    },
    setEmail(state, action: PayloadAction<string>) {
      state.email = action.payload;
    },
    setAge(state, action: PayloadAction<number>) {
      state.age = action.payload; // action.payload is typed as number
    },
    updateProfile(state, action: PayloadAction<Partial<UserState>>) {
      // action.payload is { name?: string, email?: string, age?: number }
      Object.assign(state, action.payload);
    }
  }
});

Using PayloadAction also enforces correct types when dispatching.

dispatch(setName('John Doe'));        // OK
dispatch(setName(123));               // TypeScript error!
dispatch(setAge(25));                 // OK
dispatch(setAge('twenty-five'));      // TypeScript error!

Legacy Redux vs Redux Toolkit Comparison

Let's compare the same todo app written with legacy Redux and RTK.

Legacy Redux

// actionTypes.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const SET_FILTER = 'SET_FILTER';

// actionCreators.js
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from './actionTypes';

let nextId = 1;
export const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: nextId++, text, completed: false }
});
export const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
export const deleteTodo = (id) => ({ type: DELETE_TODO, payload: id });
export const setFilter = (filter) => ({ type: SET_FILTER, payload: filter });

// reducer.js
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from './actionTypes';

const initialState = { items: [], filter: 'all' };

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return { ...state, items: [...state.items, action.payload] };
    case TOGGLE_TODO:
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        )
      };
    case DELETE_TODO:
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };
    case SET_FILTER:
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

// store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import todosReducer from './reducer';

export default createStore(
  combineReducers({ todos: todosReducer }),
  applyMiddleware(thunk)
);

Files: 4, ~60 lines of code

Redux Toolkit

// todosSlice.js
import { createSlice } from '@reduxjs/toolkit';

let nextId = 1;

const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], filter: 'all' },
  reducers: {
    addTodo(state, action) {
      state.items.push({ id: nextId++, text: action.payload, completed: false });
    },
    toggleTodo(state, action) {
      const todo = state.items.find(item => item.id === action.payload);
      if (todo) todo.completed = !todo.completed;
    },
    deleteTodo(state, action) {
      state.items = state.items.filter(item => item.id !== action.payload);
    },
    setFilter(state, action) {
      state.filter = action.payload;
    }
  }
});

export const { addTodo, toggleTodo, deleteTodo, setFilter } = todosSlice.actions;
export default todosSlice.reducer;

// store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';

export default configureStore({
  reducer: { todos: todosReducer }
});

Files: 2, ~30 lines of code

Side-by-Side Comparison

Aspect Legacy Redux Redux Toolkit
Files needed 4 files 2 files
Lines of code ~60 lines ~30 lines
Action Types Manually defined constants Auto-generated
Action Creators Manually defined functions Auto-generated
Immutable updates Spread syntax (manual) Immer (automatic)
Middleware setup applyMiddleware (manual) Auto-configured
DevTools Separate setup required Auto-configured
Type safety Manual type definitions Easy with PayloadAction

Summary

Concept Description
configureStore Creates the Store with DevTools, thunk, and dev checks auto-configured
createSlice Defines state, reducers, and actions in a single function call
Provider React component that supplies the Store to the component tree
useSelector Hook for reading state from the Store. Optimizes re-renders via reference equality
useDispatch Hook that returns the dispatch function for sending Actions
Immer Library enabling "mutable" syntax that produces immutable updates
PayloadAction TypeScript type for Actions with typed payloads
Slice A bundle of state + reducers + actions, typically organized by feature

Today we learned the fundamental Redux Toolkit APIs and built both a counter app and a todo list app. By using createSlice and configureStore, we drastically reduced the amount of code compared to legacy Redux and improved the developer experience.

Tomorrow in Day 3, we'll learn how to combine multiple slices in a practical application and explore selector patterns.


Exercises

Exercise 1: Shopping Cart Slice

Create a shopping cart slice using createSlice with the following features:

  • addItem(product): Add a product to the cart (if the same product exists, increment quantity by 1)
  • removeItem(productId): Remove a product from the cart entirely
  • updateQuantity({ productId, quantity }): Change the quantity of a product
  • clearCart(): Empty the cart

Initial state:

{
  items: [],    // { id, name, price, quantity }
  totalItems: 0,
  totalPrice: 0
}

Exercise 2: useSelector Optimization

The following code has a performance problem. Identify the issue and fix it.

function CartSummary() {
  const cartData = useSelector((state) => ({
    items: state.cart.items,
    total: state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    count: state.cart.items.length,
  }));

  return (
    <div>
      <p>Items: {cartData.count}</p>
      <p>Total: ${cartData.total}</p>
    </div>
  );
}

Exercise 3: Understanding Immer

Determine which of the following reducers will work correctly and which will not, and explain why.

const slice = createSlice({
  name: 'example',
  initialState: { items: [], count: 0 },
  reducers: {
    // A
    addItem(state, action) {
      state.items.push(action.payload);
      state.count += 1;
    },
    // B
    resetItems(state) {
      return { ...state, items: [], count: 0 };
    },
    // C
    badReset(state) {
      state.items = [];
      return state;
    },
    // D
    replaceAll(state, action) {
      return { items: action.payload, count: action.payload.length };
    }
  }
});