Free tools Windows power users keep installed
One-click scans. No signup required.
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.
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.
#1 Best Overall
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.
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.
Rank #3
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.
Rank #4
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.
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.
Best Value
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.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsChoose 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.
Quick Recap
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.

