📘 ReactJS

State Management — Context API vs Redux Toolkit

React applications start with local component state, but larger applications often need shared state. Context API and Redux Toolkit solve different problems.

S

Site Administrator

📅 Sep 28, 2026 🕒 8 min read 👁 25 views
☆ Save 𝕏 in f ✆
State Management — Context API vs Redux Toolkit

State management is where React architecture becomes especially important. A small application can use useState and props. A larger application may need Context, reducers, Redux Toolkit, selectors, async workflows, normalized data, persistence, and clear boundaries between client state and server state. This chapter includes a complete Redux Toolkit example instead of stopping at definitions.

Understand the different kinds of state

Before selecting a state-management tool, identify what kind of state you have. Local UI state, shared client state, authentication state, form state, and server state have different lifecycles.

flowchart TD
A[Application data] --> B[Local UI state]
A --> C[Shared client state]
A --> D[Authentication state]
A --> E[Form state]
A --> F[Server state]
B --> G[useState or useReducer]
C --> H[Context or Redux Toolkit]
D --> I[Context or state store]
E --> J[Form library]
F --> K[API or server-state solution]

Start with local state

If only one component needs a value, do not move it into global state simply because Redux or Context exists.

function QuantityPicker() {
  const [quantity, setQuantity] = useState(1);

  return (
    <button onClick={() => setQuantity(q => q + 1)}>
      Quantity: {quantity}
    </button>
  );
}

Context API complete example

Context is excellent when the main requirement is making a value available to a component subtree.

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  const value = useMemo(
    () => ({
      theme,
      toggleTheme: () =>
        setTheme(current =>
          current === "light" ? "dark" : "light"
        )
    }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header className={theme}>
      <button onClick={toggleTheme}>
        Theme: {theme}
      </button>
    </header>
  );
}

Why Context is not automatically Redux

Context provides dependency sharing through a component tree. It does not automatically provide a complete architecture for actions, reducers, selectors, middleware, debugging, or complex state transitions. You can build those things yourself, but Redux Toolkit already provides a structured approach for them.

Redux Toolkit architecture

flowchart LR
A[React component] --> B[dispatch action]
B --> C[Redux store]
C --> D[Slice reducer]
D --> C
C --> E[Selector]
E --> A
F[API request] --> G[Async thunk]
G --> B

Install Redux Toolkit

npm install @reduxjs/toolkit react-redux

Redux Toolkit contains the core Redux tools. react-redux connects the Redux store to React components.

Create a real cart slice

Consider an e-commerce cart. The cart is shared by the product list, product details, header, checkout page, and order summary. A slice can own the cart state and actions.

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

const cartSlice = createSlice({
  name: "cart",

  initialState: {
    items: [],
    coupon: null
  },

  reducers: {
    addItem(state, action) {
      const product = action.payload;

      const existing = state.items.find(
        item => item.productId === product.productId
      );

      if (existing) {
        existing.quantity += product.quantity;
      } else {
        state.items.push(product);
      }
    },

    removeItem(state, action) {
      state.items = state.items.filter(
        item => item.productId !== action.payload
      );
    },

    changeQuantity(state, action) {
      const { productId, quantity } = action.payload;

      const item = state.items.find(
        item => item.productId === productId
      );

      if (item) {
        item.quantity = quantity;
      }
    },

    clearCart(state) {
      state.items = [];
      state.coupon = null;
    }
  }
});

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

export default cartSlice.reducer;

The mutation-like syntax is handled safely by Redux Toolkit's Immer integration. The resulting state update remains immutable from Redux's perspective.

Create the Redux store

import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";

export const store = configureStore({
  reducer: {
    cart: cartReducer
  }
});

Connect Redux to React

import { Provider } from "react-redux";
import { store } from "./store";

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

Dispatching an action

import { useDispatch } from "react-redux";
import { addItem } from "./cartSlice";

function ProductCard({ product }) {
  const dispatch = useDispatch();

  function addToCart() {
    dispatch(
      addItem({
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      })
    );
  }

  return (
    <button onClick={addToCart}>
      Add to Cart
    </button>
  );
}

Reading state with useSelector

import { useSelector } from "react-redux";

function CartBadge() {
  const count = useSelector(
    state => state.cart.items.reduce(
      (total, item) => total + item.quantity,
      0
    )
  );

  return <span>Cart ({count})</span>;
}

Create selectors

Selectors keep state-access logic reusable and make components easier to read.

export const selectCartItems =
  state => state.cart.items;

export const selectCartCount = state =>
  state.cart.items.reduce(
    (total, item) => total + item.quantity,
    0
  );

export const selectCartTotal = state =>
  state.cart.items.reduce(
    (total, item) =>
      total + item.price * item.quantity,
    0
  );

Complete cart output

Suppose the user adds a laptop priced at ₹80,000 and a mouse priced at ₹2,000.

Visible output:

Products
Laptop       ₹80,000   [Add to Cart]
Mouse         ₹2,000   [Add to Cart]

Header:
Cart (2)

Cart Summary:
Laptop        ₹80,000 x 1
Mouse          ₹2,000 x 1
-----------------------
Subtotal      ₹82,000

If the user adds another mouse, the same product item is updated instead of creating an unnecessary duplicate row.

Cart (3)

Laptop        ₹80,000 x 1
Mouse          ₹2,000 x 2
-----------------------
Subtotal      ₹84,000

Async Redux with createAsyncThunk

Redux Toolkit also provides createAsyncThunk for common asynchronous workflows. A realistic example is loading orders from a backend API.

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

export const fetchOrders = createAsyncThunk(
  "orders/fetchOrders",
  async () => {
    const response = await fetch("/api/orders");

    if (!response.ok) {
      throw new Error("Unable to load orders");
    }

    return response.json();
  }
);

Handle pending fulfilled rejected

const ordersSlice = createSlice({
  name: "orders",

  initialState: {
    items: [],
    status: "idle",
    error: null
  },

  reducers: {},

  extraReducers: builder => {
    builder
      .addCase(fetchOrders.pending, state => {
        state.status = "loading";
        state.error = null;
      })
      .addCase(fetchOrders.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.items = action.payload;
      })
      .addCase(fetchOrders.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  }
});

Orders component output

function Orders() {
  const dispatch = useDispatch();

  const {
    items,
    status,
    error
  } = useSelector(state => state.orders);

  useEffect(() => {
    if (status === "idle") {
      dispatch(fetchOrders());
    }
  }, [status, dispatch]);

  if (status === "loading") {
    return <p>Loading orders...</p>;
  }

  if (status === "failed") {
    return <p>Error: {error}</p>;
  }

  if (items.length === 0) {
    return <p>No orders found.</p>;
  }

  return (
    <ul>
      {items.map(order => (
        <li key={order.id}>
          {order.id} - {order.status}
        </li>
      ))}
    </ul>
  );
}

Complete async output

Initial:
Loading orders...

Success:
Orders
ORD-1001 - Approved
ORD-1002 - Pending
ORD-1003 - Shipped

Failure:
Unable to load orders

Redux middleware

Redux middleware sits between dispatching an action and the reducer processing it. It is useful for async workflows, logging, analytics, or other cross-cutting behavior. Redux Toolkit configures useful defaults for modern Redux applications.

Redux DevTools

Redux DevTools can show dispatched actions and state changes, which is valuable when debugging complex state transitions. Instead of asking where a value changed, developers can inspect the action sequence that produced the state.

Normalize complex collections

Large entity collections can become inefficient if the same object is duplicated in multiple places. Redux Toolkit provides utilities such as createEntityAdapter for normalized entity state when the application needs that pattern.

Context vs Redux Toolkit

ConcernContextRedux Toolkit
Tree-wide value sharingExcellentPossible
Complex state transitionsManual architectureStructured with slices
Action historyNot built inBuilt around actions
Async workflowsManualStrong tooling
Debugging toolsBasicStrong Redux tooling
Small global valueOften enoughMay be unnecessary

What should NOT automatically go into Redux

  • Temporary input text used by one component.
  • Simple modal visibility local to one screen.
  • Component-specific hover state.
  • Every API response just because it came from a server.
  • Values that have no shared consumers.

A production state architecture

flowchart TD
A[React app] --> B[Local state]
A --> C[Context]
A --> D[Redux Toolkit]
A --> E[API layer]
B --> F[Local UI behavior]
C --> G[Theme Auth Config]
D --> H[Complex shared client state]
E --> I[Backend]
I --> J[Server data]

Common mistakes

  • Putting all application data into Redux without classification.
  • Creating huge slices with unrelated business domains.
  • Writing selectors directly inside dozens of components.
  • Duplicating server state unnecessarily.
  • Using Context for rapidly changing large data without considering rendering behavior.
  • Using Redux simply because the project is large.

Interview questions

  • Context vs Redux Toolkit?
  • What is a slice?
  • What is dispatch?
  • What is a selector?
  • What does createAsyncThunk do?
  • How are loading, success, and failure represented?
  • Why does Redux Toolkit allow mutation-like reducer code?
  • When should state remain local?

Key takeaways

  • Classify state before choosing a tool.
  • Context is excellent for dependency sharing through a tree.
  • Redux Toolkit gives complex shared client state a predictable architecture.
  • Use slices, selectors, actions, and async workflows to keep Redux maintainable.
  • Do not make global state a dumping ground.

Comments & Ratings (0)

Log in or create a student account to comment and rate this article.

Be the first to comment on this article.

Never miss a new post 📬

Get an email whenever a new article, project, cheatsheet or video drops.

✅

ℹ️