Global State with Redux Toolkit

Reviewed & published by Brayan K

When many unrelated parts of an app need the same data — the logged-in user, a shopping cart, a theme — passing props down through every layer gets painful. Redux puts that shared state in one predictable store. Redux Toolkit (RTK) is the official, boilerplate-free way to use it: createSlice generates your actions and lets you write update logic that looks mutable but stays immutable. This is the industrial-strength state pattern you'll see in large React codebases.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ The Redux Model

Four ideas: the store holds state; an action describes "what happened"; a reducer computes the next state from the current state plus an action; dispatch sends an action through. The reducer is a pure (state, action) => newState function:

The demo models this engine — dispatch a few actions and watch the single state evolve:

// At its core, Redux is one function: (state, action) => newState.
// RTK generates this for you, but here's the engine, modeled in JS.

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

// dispatch = "run the reducer with this action, store the result"
let state = { value: 0 };
function dispatch(action) { state = counterReducer(state, action); }

dispatch({ type: "counter/increment" });
dispatch({ type: "counter/increment" });
dispatch({ type: "counter/addBy", payload: 10 });

console.log(state.value);   // 12
// createSlice hides this switch and generates the action objects for you.

2️⃣ createSlice — No Boilerplate

Classic Redux made you hand-write action type strings and immutable spreads. RTK's createSlice generates the action creators and, thanks to Immer, lets you write code that looks like mutation but produces a new immutable state:

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },        // looks mutable…
    decrement: (state) => { state.value -= 1; },        // …Immer makes it safe
    addBy:     (state, action) => { state.value += action.payload; },
  },
});

// RTK auto-generates one action creator per reducer:
export const { increment, decrement, addBy } = counterSlice.actions;
export default counterSlice.reducer;

Your turn — complete the reducer that appends an item to the cart array:

// 🎯 YOUR TURN — fill in the ___ then press "Try it Yourself".
// In a slice, each reducer returns the next state. Complete "addBy".

function reducer(state, action) {
  switch (action.type) {
    case "cart/addItem":
      // 1) Add the item from action.payload onto the items array.
      return { items: [...state.items, ___] };   // 👉 use action.payload
    default:
      return state;
  }
}

let state = { items: [] };
state = reducer(state, { type: "cart/addItem", payload: "shoes" });
state = reducer(state, { type: "cart/addItem", payload: "hat" });

console.log(state.items);

// ✅ Expected output:
//    [ 'shoes', 'hat' ]

3️⃣ Store, Provider, and the Hooks

Combine your slices into one store, wrap the app in a Provider, then read and write from any component:

// store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

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

// main.tsx — wrap once
import { Provider } from "react-redux";
<Provider store={store}><App /></Provider>

// Counter.tsx — read + write anywhere
import { useSelector, useDispatch } from "react-redux";
import { increment, addBy } from "./counterSlice";

function Counter() {
  const count = useSelector((state) => state.counter.value);   // subscribe
  const dispatch = useDispatch();
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(addBy(10))}>+10</button>
    </div>
  );
}

A selector reads just the slice a component needs — so it only re-renders when that value changes. The demo models several selectors over one store:

// useSelector reads a SLICE of the whole store. We model the store as
// one object and selectors as functions that pick a piece out of it.

const store = {
  counter: { value: 7 },
  user:    { name: "Ada", isAdmin: true },
  cart:    { items: ["book", "pen"] },
};

// Each selector grabs exactly what a component needs:
const selectCount    = (s) => s.counter.value;
const selectUserName  = (s) => s.user.name;
const selectCartCount = (s) => s.cart.items.length;

console.log("count:", selectCount(store));       // count: 7
console.log("user:", selectUserName(store));     // user: Ada
console.log("cart size:", selectCartCount(store)); // cart size: 2
// A component re-renders only when ITS selected value changes.

Reorder Challenge

These lines of a createSlice call are scrambled. Put them in the correct order:

A.     reducers: {
B.   const slice = createSlice({
C.       increment: (state) => { state.value += 1; },
D.     initialState: { value: 0 },
E.     },
F.     name: "counter",
G.   });

Quick Recall

1) How does a component read a value from the Redux store?

useSelector((state) => state.slice.value) — it subscribes the component to just that selected piece.

2) Why is state.value += 1 allowed inside a slice reducer?

RTK wraps reducers with Immer, which turns your "mutations" into a correct immutable update behind the scenes.

3) You changed state.user.name in the store. Which components re-render?

Only those whose useSelector result actually changed — e.g. ones selecting the user name. Components selecting unrelated slices don't re-render.

Common Errors (and the fix)

📋 Quick Reference

TaskSyntax
Create a slicecreateSlice({ name, initialState, reducers })
Build the storeconfigureStore({ reducer: { counter } })
Provide it<Provider store={store}>
Read stateuseSelector(s => s.counter.value)
Dispatchdispatch(increment())

Mini-Challenge: Toggle Slice

Model a tiny theme slice reducer with a toggle action that flips between "light" and "dark".

// A theme slice reducer that toggles light <-> dark.
function reducer(state, action) {
  if (action.type === "theme/toggle") {
    // your code here: return { mode: state.mode === "light" ? "dark" : "light" }
  }
  return state;
}

let state = { mode: "light" };
state = reducer(state, { type: "theme/toggle" });
console.log(state.mode);
state = reducer(state, { type: "theme/toggle" });
console.log(state.mode);

// 🎯 Expected:
//    dark
//    light

🎉 Lesson Complete

Practice quiz

What is Redux Toolkit (RTK)?

  • A CSS-in-JS library
  • A replacement for React
  • The official, recommended way to write Redux with less boilerplate
  • A router

Answer: The official, recommended way to write Redux with less boilerplate. RTK is the official toolset; createSlice generates action types/creators and reduces boilerplate.

What is the shape of a Redux reducer?

  • (state, action) => newState
  • () => state
  • (action) => state
  • (state) => action

Answer: (state, action) => newState. A reducer is a pure function (state, action) => newState.

What does createSlice do?

  • Creates a React component
  • Connects to a database
  • Configures routing
  • Bundles a feature's initialState and reducers, auto-generating action creators

Answer: Bundles a feature's initialState and reducers, auto-generating action creators. createSlice({ name, initialState, reducers }) generates an action creator per reducer.

Why is state.value += 1 allowed inside a slice reducer?

  • Redux always allows mutation
  • RTK wraps reducers with Immer, which turns 'mutations' into a correct immutable update
  • It's a syntax error that React fixes
  • Because value is a primitive

Answer: RTK wraps reducers with Immer, which turns 'mutations' into a correct immutable update. Immer records the mutations and builds a new immutable state behind the scenes — only inside slice reducers.

How does a component read a value from the store?

  • useSelector((state) => state.counter.value)
  • useDispatch
  • useStore directly
  • useState

Answer: useSelector((state) => state.counter.value). useSelector subscribes the component to just the selected slice of state.

How does a component update the store?

  • Mutate state directly
  • Call the reducer directly
  • const dispatch = useDispatch(); dispatch(increment())
  • setState

Answer: const dispatch = useDispatch(); dispatch(increment()). Dispatch an action creator's result; the store runs the reducer and notifies subscribers.

What does configureStore do?

  • Creates a slice
  • Combines reducers into one store (with good defaults)
  • Provides the store to React
  • Reads state

Answer: Combines reducers into one store (with good defaults). configureStore({ reducer: { counter } }) builds the store from your slice reducers.

What wires the Redux store into the React app?

  • <Router>
  • <StoreContext>
  • configureStore alone
  • <Provider store={store}>

Answer: <Provider store={store}>. Wrapping the app in react-redux's <Provider store={store}> makes the store available to hooks.

After a store update, which components re-render?

  • All of them
  • Only those whose useSelector result actually changed
  • None until refresh
  • Only the dispatching one

Answer: Only those whose useSelector result actually changed. Components re-render only when their selected value changes; unrelated selectors don't.

Where is mutating state with Immer-style syntax valid?

  • Anywhere in the app
  • Only in selectors
  • Only inside RTK slice reducers; elsewhere return a new object/array
  • In useEffect

Answer: Only inside RTK slice reducers; elsewhere return a new object/array. Immer only applies inside slice reducers; outside, you must produce a new immutable value.

Continue this course

Related lessons