Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
FREEDOM251

Freedom251 › Blog › JavaScript

How to Choose React State Management in 2026: One Cart in Context, Redux Toolkit, Zustand, and Jotai

By Freedom251 Team · · 10 min read

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a small cart shared within one part of a React app, Context may be enough. Choose Redux Toolkit when your team wants Redux’s recommended conventions and centralized update flow; Zustand for a store-oriented model without an app-wide provider wrapper; or Jotai when composing state from atoms suits the domain. None is a universal performance winner: the right fit depends on how you organize updates, derived values, subscriptions, and team conventions.

Here is the same cart implemented four ways. Each version starts with the same products, supports adding and removing items and changing quantity, and derives item count and subtotal. The examples illustrate each model; they are not a benchmark.

As an Amazon Associate I earn from qualifying purchases.

What the comparison holds constant

Each cart item has a product ID, name, unit price, and quantity. Adding a product that is already present increases its quantity; removing a product deletes it; changing quantity replaces its quantity. The examples start with two teas at $12 each and one mug at $8, for three items and a $32 subtotal.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
type CartItem = {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
};

const initialItems: CartItem[] = [
  { productId: "tea", name: "Tea", unitPrice: 12, quantity: 2 },
  { productId: "mug", name: "Mug", unitPrice: 8, quantity: 1 },
];

// Shared meaning in all four examples:
// add tea with quantity 1 -> tea quantity becomes 3
// remove mug              -> mug is removed
// set tea quantity to 4   -> 4 items, $48 subtotal

All examples assume the product information is already available to the client. A cart’s client-side interaction state is a different concern from fetching and caching product or account data from a server; this comparison does not choose a server-data library.

Context: pass a cart through a subtree

Context is a React API for making a value available to components deeper in a subtree. It does not, by itself, provide a selector-based state-management framework. A provider can own the cart reducer, and consumers can read the resulting value with useContext. React documents that a component reading a context updates when that context’s value changes; that is not the same as saying every descendant, including components that do not read the context, necessarily updates. See React’s createContext and useContext references.

State, actions, and provider

import { createContext, useContext, useReducer } from "react";

type CartItem = {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
};

type Action =
  | { type: "add"; item: Omit<CartItem, "quantity">; quantity?: number }
  | { type: "remove"; productId: string }
  | { type: "setQuantity"; productId: string; quantity: number };

type CartValue = { items: CartItem[]; dispatch: (action: Action) => void };

const initialItems: CartItem[] = [
  { productId: "tea", name: "Tea", unitPrice: 12, quantity: 2 },
  { productId: "mug", name: "Mug", unitPrice: 8, quantity: 1 },
];

function reducer(items: CartItem[], action: Action): CartItem[] {
  switch (action.type) {
    case "add": {
      const quantity = action.quantity ?? 1;
      const existing = items.find((item) => item.productId === action.item.productId);
      return existing
        ? items.map((item) =>
            item.productId === action.item.productId
              ? { ...item, quantity: item.quantity + quantity }
              : item
          )
        : [...items, { ...action.item, quantity }];
    }
    case "remove":
      return items.filter((item) => item.productId !== action.productId);
    case "setQuantity":
      return action.quantity < 1
        ? items.filter((item) => item.productId !== action.productId)
        : items.map((item) =>
            item.productId === action.productId
              ? { ...item, quantity: action.quantity }
              : item
          );
  }
}

const CartContext = createContext<CartValue | null>(null);

function CartProvider({ children }: { children: React.ReactNode }) {
  const [items, dispatch] = useReducer(reducer, initialItems);
  return (
    <CartContext.Provider value={{ items, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

function useCart() {
  const value = useContext(CartContext);
  if (!value) throw new Error("useCart must be used inside CartProvider");
  return value;
}

Consumer and update flow

function CartSummary() {
  const { items, dispatch } = useCart();
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
  const subtotal = items.reduce(
    (sum, item) => sum + item.unitPrice * item.quantity,
    0
  );

  return (
    <section>
      <p>Items: {itemCount}</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <button onClick={() => dispatch({
        type: "add",
        item: { productId: "tea", name: "Tea", unitPrice: 12 },
      })}>Add tea</button>
      <button onClick={() => dispatch({ type: "remove", productId: "mug" })}>
        Remove mug
      </button>
      <button onClick={() => dispatch({
        type: "setQuantity", productId: "tea", quantity: 4,
      })}>Set tea quantity to 4</button>
    </section>
  );
}

function App() {
  return (
    <CartProvider>
      <CartSummary />
    </CartProvider>
  );
}

In this example, the summary consumes the full context value and calculates both derived values from the cart. If the context value changes on a cart update, that consumer updates. For a small, naturally scoped shared value, this directness can be an advantage. If many components consume one large, frequently changing context object, consider whether the resulting subscription scope and organization still suit the app.

React 19 also permits rendering the context object itself as the provider, such as <CartContext value={...}>. In earlier React versions, use <CartContext.Provider value={...}>, as above.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Redux Toolkit: centralize updates in a slice and store

Redux Toolkit is the Redux team’s officially recommended way to write Redux logic. Its documented approach streamlines store setup, reducers, immutable update logic, and common Redux tasks. This cart puts update rules in a slice, dispatches named actions, and selects the derived values where they are displayed. See the Redux Getting Started guide and the Redux Toolkit Usage Guide.

Slice and store

import { configureStore, createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";

type CartItem = {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
};

type CartState = { items: CartItem[] };

const initialState: CartState = {
  items: [
    { productId: "tea", name: "Tea", unitPrice: 12, quantity: 2 },
    { productId: "mug", name: "Mug", unitPrice: 8, quantity: 1 },
  ],
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    addItem(state, action: PayloadAction<Omit<CartItem, "quantity"> & { quantity?: number }>) {
      const quantity = action.payload.quantity ?? 1;
      const existing = state.items.find(
        (item) => item.productId === action.payload.productId
      );
      if (existing) existing.quantity += quantity;
      else state.items.push({ ...action.payload, quantity });
    },
    removeItem(state, action: PayloadAction<string>) {
      state.items = state.items.filter((item) => item.productId !== action.payload);
    },
    setQuantity(state, action: PayloadAction<{ productId: string; quantity: number }>) {
      const { productId, quantity } = action.payload;
      if (quantity < 1) {
        state.items = state.items.filter((item) => item.productId !== productId);
      } else {
        const item = state.items.find((candidate) => candidate.productId === productId);
        if (item) item.quantity = quantity;
      }
    },
  },
});

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

const store = configureStore({
  reducer: { cart: cartSlice.reducer },
});

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

Selectors and consumer

function CartSummary() {
  const dispatch = useDispatch<AppDispatch>();
  const items = useSelector((state: RootState) => state.cart.items);
  const itemCount = useSelector((state: RootState) =>
    state.cart.items.reduce((sum, item) => sum + item.quantity, 0)
  );
  const subtotal = useSelector((state: RootState) =>
    state.cart.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)
  );

  return (
    <section>
      <p>Items: {itemCount}</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <button onClick={() => dispatch(addItem({
        productId: "tea", name: "Tea", unitPrice: 12,
      }))}>Add tea</button>
      <button onClick={() => dispatch(removeItem("mug"))}>Remove mug</button>
      <button onClick={() => dispatch(setQuantity({ productId: "tea", quantity: 4 }))}>
        Set tea quantity to 4
      </button>
    </section>
  );
}

function App() {
  return (
    <Provider store={store}>
      <CartSummary />
    </Provider>
  );
}

The slice makes the allowed cart changes explicit and keeps update logic in one place. That structure can help when a team values predictable conventions and a centralized workflow; it also introduces Redux’s store, action, reducer, and provider concepts. The selectors show where the count and subtotal come from rather than storing duplicate totals that can drift out of sync.

Zustand: use a store without an app-wide provider wrapper

Zustand keeps state and actions in a store and lets components select what they need. Its comparison with Redux documents a setup distinction: Redux integrations use context providers, while Zustand does not require wrapping the app in one. That is a difference in setup, not evidence that Zustand is always faster or smaller. See Zustand’s comparison.

Store and actions

import { create } from "zustand";

type CartItem = {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
};

type CartStore = {
  items: CartItem[];
  addItem: (item: Omit<CartItem, "quantity">, quantity?: number) => void;
  removeItem: (productId: string) => void;
  setQuantity: (productId: string, quantity: number) => void;
};

const useCartStore = create<CartStore>((set) => ({
  items: [
    { productId: "tea", name: "Tea", unitPrice: 12, quantity: 2 },
    { productId: "mug", name: "Mug", unitPrice: 8, quantity: 1 },
  ],
  addItem: (newItem, quantity = 1) =>
    set((state) => {
      const existing = state.items.find((item) => item.productId === newItem.productId);
      return {
        items: existing
          ? state.items.map((item) =>
              item.productId === newItem.productId
                ? { ...item, quantity: item.quantity + quantity }
                : item
            )
          : [...state.items, { ...newItem, quantity }],
      };
    }),
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((item) => item.productId !== productId),
    })),
  setQuantity: (productId, quantity) =>
    set((state) => ({
      items: quantity < 1
        ? state.items.filter((item) => item.productId !== productId)
        : state.items.map((item) =>
            item.productId === productId ? { ...item, quantity } : item
          ),
    })),
}));

Selectors and consumer

function CartSummary() {
  const itemCount = useCartStore((state) =>
    state.items.reduce((sum, item) => sum + item.quantity, 0)
  );
  const subtotal = useCartStore((state) =>
    state.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)
  );
  const addItem = useCartStore((state) => state.addItem);
  const removeItem = useCartStore((state) => state.removeItem);
  const setQuantity = useCartStore((state) => state.setQuantity);

  return (
    <section>
      <p>Items: {itemCount}</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <button onClick={() => addItem({
        productId: "tea", name: "Tea", unitPrice: 12,
      })}>Add tea</button>
      <button onClick={() => removeItem("mug")}>Remove mug</button>
      <button onClick={() => setQuantity("tea", 4)}>Set tea quantity to 4</button>
    </section>
  );
}

function App() {
  return <CartSummary />;
}

The store’s selectors express which values the summary reads, while actions live alongside the cart state. This can feel lightweight when a team wants a store model without the provider setup shown for Redux integration. Evaluate any additional needs—such as persistence, middleware, or store scoping—against the current documentation and your application rather than assuming this minimal example covers them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Jotai: compose the cart from atoms

Jotai models state with atoms and lets derived atoms express dependencies between values. Its comparison characterizes Jotai as a bottom-up atomic model and Zustand as a top-down single-object store model. An atom can represent the cart collection, while derived atoms compute count and subtotal; an action atom can keep updates together without requiring one atom per individual field. See the Jotai comparison and Jotai documentation.

Cart and derived atoms

import { atom, Provider, useAtomValue, useSetAtom } from "jotai";

type CartItem = {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
};

type CartAction =
  | { type: "add"; item: Omit<CartItem, "quantity">; quantity?: number }
  | { type: "remove"; productId: string }
  | { type: "setQuantity"; productId: string; quantity: number };

const cartItemsAtom = atom<CartItem[]>([
  { productId: "tea", name: "Tea", unitPrice: 12, quantity: 2 },
  { productId: "mug", name: "Mug", unitPrice: 8, quantity: 1 },
]);

const itemCountAtom = atom((get) =>
  get(cartItemsAtom).reduce((sum, item) => sum + item.quantity, 0)
);

const subtotalAtom = atom((get) =>
  get(cartItemsAtom).reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)
);

const cartActionAtom = atom(null, (get, set, action: CartAction) => {
  const items = get(cartItemsAtom);
  switch (action.type) {
    case "add": {
      const quantity = action.quantity ?? 1;
      const existing = items.find((item) => item.productId === action.item.productId);
      set(cartItemsAtom, existing
        ? items.map((item) =>
            item.productId === action.item.productId
              ? { ...item, quantity: item.quantity + quantity }
              : item
          )
        : [...items, { ...action.item, quantity }]
      );
      break;
    }
    case "remove":
      set(cartItemsAtom, items.filter((item) => item.productId !== action.productId));
      break;
    case "setQuantity":
      set(cartItemsAtom, action.quantity < 1
        ? items.filter((item) => item.productId !== action.productId)
        : items.map((item) =>
            item.productId === action.productId
              ? { ...item, quantity: action.quantity }
              : item
          )
      );
      break;
  }
});

Consumer and optional provider

function CartSummary() {
  const itemCount = useAtomValue(itemCountAtom);
  const subtotal = useAtomValue(subtotalAtom);
  const dispatchCartAction = useSetAtom(cartActionAtom);

  return (
    <section>
      <p>Items: {itemCount}</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <button onClick={() => dispatchCartAction({
        type: "add",
        item: { productId: "tea", name: "Tea", unitPrice: 12 },
      })}>Add tea</button>
      <button onClick={() => dispatchCartAction({ type: "remove", productId: "mug" })}>
        Remove mug
      </button>
      <button onClick={() => dispatchCartAction({
        type: "setQuantity", productId: "tea", quantity: 4,
      })}>Set tea quantity to 4</button>
    </section>
  );
}

function App() {
  return (
    <Provider>
      <CartSummary />
    </Provider>
  );
}

The count and subtotal atoms depend on the cart atom, so their definitions remain centralized and update when the cart changes. The provider is included to establish a Jotai store scope for this example; atom composition, rather than the provider alone, is the defining state model here.

What the benchmark evidence does—and does not—show

A title-matched DEV Community article attributed to PrepStack reports component render counts and timings for a 1,000-component test, along with figures concerning bundle size, interactions, filtering, code lines, and onboarding. Those are that page’s reported results, not independently established measurements here. The material available does not establish enough about implementation parity, React version, build mode, environment, repetitions, or measurement method to treat its figures as a reliable general ranking.

These code examples were not run as a benchmark. A useful comparison would publish equivalent implementations and the exact runtime, React version, build configuration, hardware, workload, warm-up, number of repetitions, and raw sample distribution. It would also define what a render count means—for example, component function calls versus committed updates, and whether parent renders or development Strict Mode behavior are included. Without that protocol, a precise-looking number can answer a different question from the one your app needs answered.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose by state model and team needs

  • Use Context when a value belongs to a subtree and sharing it through React’s built-in mechanism is simple enough for the update and consumer pattern.
  • Use Redux Toolkit when a team wants the Redux team’s recommended conventions, named actions, reducers, and centralized update flow.
  • Use Zustand when a store-oriented model and its setup style fit the app, including the documented absence of a required app-wide provider wrapper.
  • Use Jotai when composing related and derived state from atoms is a more natural way to represent the domain.

Before committing, model a representative feature—not just a counter. Check where update rules live, whether derived data is duplicated, which components need which values, how new teammates will learn the conventions, and what debugging or testing workflow the project requires. These examples make those trade-offs visible; they do not establish a speed, bundle-size, or productivity winner.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

JavaScriptJotaiReactRedux ToolkitState ManagementZustand

Leave a Reply

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

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.