ItemQuantity
Description
A headless component that provides quantity management for cart items with automatic cart updates, debouncing, min/max validation, and loading states. Available as both a render props component and a useItemQuantity hook.
Role in Theming
ItemQuantity is one of EverShop's headless components — it owns the quantity update logic while leaving all UI decisions to its parent:
- ItemQuantity renders nothing. It returns only what its
childrenfunction renders. - Theme developers do not override ItemQuantity. Instead, they override the parent components that consume it (typically
DefaultCartItemListwhich uses it inside each cart item row). - The logic stays stable across themes. Debounced cart updates, min/max validation, optimistic UI, and error recovery are all encapsulated in ItemQuantity.
ItemQuantity is unique among EverShop's headless components because it also exposes its logic as a hook (useItemQuantity), giving theme developers a choice between the render props pattern and direct hook usage.
Theme Override Points
ItemQuantity is consumed inside cart item components. Theme developers override the parent to change the quantity UI:
| Parent Component | Route | Override Path in Theme |
|---|---|---|
| DefaultCartItemList | cart | themes/<name>/src/pages/cart/DefaultCartItemList.tsx |
Import
import { ItemQuantity, useItemQuantity } from '@components/frontStore/cart/ItemQuantity';
Usage
As Render Props Component
import { ItemQuantity } from '@components/frontStore/cart/ItemQuantity';
function CartItem({ item }) {
return (
<ItemQuantity cartItemId={item.cartItemId} initialValue={item.qty}>
{({ quantity, increase, decrease, loading }) => (
<div>
<button onClick={decrease} disabled={loading}>-</button>
<span>{quantity}</span>
<button onClick={increase} disabled={loading}>+</button>
</div>
)}
</ItemQuantity>
);
}
As Hook
import { useItemQuantity } from '@components/frontStore/cart/ItemQuantity';
function CartItem({ item }) {
const { quantity, increase, decrease, inputProps } = useItemQuantity({
cartItemId: item.cartItemId,
initialValue: item.qty,
min: 1,
max: 10
});
return (
<div>
<button onClick={decrease}>-</button>
<input {...inputProps} />
<button onClick={increase}>+</button>
</div>
);
}
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| cartItemId | string | Yes | - | Cart item ID to update |
| initialValue | number | No | 1 | Initial quantity value |
| min | number | No | 1 | Minimum quantity allowed |
| max | number | No | Infinity | Maximum quantity allowed |
| debounce | number | No | 500 | Debounce delay in ms |
| onChange | (qty: number) => void | No | - | Called when quantity changes |
| onSuccess | () => void | No | - | Called on successful update |
| onFailure | (error: Error) => void | No | - | Called on update failure |
| children | RenderFunction | Yes* | - | Render function (component only) |
Return Values
| Name | Type | Description |
|---|---|---|
| quantity | number | Current quantity value |
| loading | boolean | True when cart is updating |
| increase | () => void | Increment quantity by 1 |
| decrease | () => void | Decrement quantity by 1 |
| setQuantity | (qty: number) => void | Set specific quantity |
| inputProps | object | Props to spread on input element |
Examples
With Input Field (Hook)
import { useItemQuantity } from '@components/frontStore/cart/ItemQuantity';
function QuantityInput({ item }) {
const { inputProps, increase, decrease, loading } = useItemQuantity({
cartItemId: item.cartItemId,
initialValue: item.qty,
min: 1,
max: 99
});
return (
<div className="flex gap-2">
<button onClick={decrease} disabled={loading}>-</button>
<input {...inputProps} className="w-16 text-center" />
<button onClick={increase} disabled={loading}>+</button>
</div>
);
}
With Callbacks
import { useItemQuantity } from '@components/frontStore/cart/ItemQuantity';
import { useState } from 'react';
function CartItem({ item }) {
const [message, setMessage] = useState('');
const { quantity, increase, decrease, loading } = useItemQuantity({
cartItemId: item.cartItemId,
initialValue: item.qty,
debounce: 300,
onSuccess: () => {
setMessage('Updated successfully');
setTimeout(() => setMessage(''), 2000);
},
onFailure: (error) => {
setMessage(`Error: ${error.message}`);
}
});
return (
<div>
<button onClick={decrease}>-</button>
<span>{quantity}</span>
<button onClick={increase}>+</button>
{loading && <span>Updating...</span>}
{message && <div>{message}</div>}
</div>
);
}
Theme Override Example
A theme developer overrides the cart item layout and uses useItemQuantity hook for a custom quantity control:
themes/my-theme/src/pages/cart/DefaultCartItemList.tsx
import { CartItems } from '@components/frontStore/cart/CartItems';
import { useItemQuantity } from '@components/frontStore/cart/ItemQuantity';
function QuantityControl({ item }) {
const { quantity, increase, decrease, loading } = useItemQuantity({
cartItemId: item.cartItemId,
initialValue: item.qty,
min: 1,
max: 20
});
return (
<select
value={quantity}
onChange={(e) => {/* handled by hook */}}
disabled={loading}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
);
}
function DefaultCartItemList() {
return (
<CartItems>
{({ items, isEmpty, onRemoveItem }) => {
if (isEmpty) return <p>Your cart is empty</p>;
return (
<div className="my-theme-cart">
{items.map(item => (
<div key={item.cartItemId} className="my-theme-cart-row">
<span>{item.productName}</span>
<QuantityControl item={item} />
<span>{item.subTotal.text}</span>
<button onClick={() => onRemoveItem(item.cartItemId)}>
Remove
</button>
</div>
))}
</div>
);
}}
</CartItems>
);
}
export default DefaultCartItemList;
export const layout = {
areaId: 'shoppingCartLeft',
sortOrder: 10
};
Behavior
Debouncing
Updates are debounced by default (500ms). Set debounce={0} for immediate updates. The decrease and increase functions use debouncing, but blur events trigger immediate updates.
Validation
Quantity is clamped between min and max values. Invalid input is ignored, empty input becomes 0 and is clamped on blur.
Cart Updates
Uses cartDispatch.updateItem with action: 'increase' or action: 'decrease' and the quantity difference. On failure, reverts to previous quantity.
Features
- Headless: Renders no UI — full control via render props or hook
- Debounced Updates: Waits 500ms before updating cart (configurable)
- Min/Max Validation: Enforces quantity limits
- Loading State: Disables controls during updates
- Error Recovery: Reverts to previous value on failure
- Input Integration: Pre-configured input props via
inputProps - Optimistic UI: Updates immediately, syncs with server
Related Components
- CartItems - Cart display component
- CartContext - Cart state management
- AddToCart - Add to cart component
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.