Skip to main content

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 children function renders.
  • Theme developers do not override ItemQuantity. Instead, they override the parent components that consume it (typically DefaultCartItemList which 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 ComponentRouteOverride Path in Theme
DefaultCartItemListcartthemes/<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

NameTypeRequiredDefaultDescription
cartItemIdstringYes-Cart item ID to update
initialValuenumberNo1Initial quantity value
minnumberNo1Minimum quantity allowed
maxnumberNoInfinityMaximum quantity allowed
debouncenumberNo500Debounce delay in ms
onChange(qty: number) => voidNo-Called when quantity changes
onSuccess() => voidNo-Called on successful update
onFailure(error: Error) => voidNo-Called on update failure
childrenRenderFunctionYes*-Render function (component only)

Return Values

NameTypeDescription
quantitynumberCurrent quantity value
loadingbooleanTrue when cart is updating
increase() => voidIncrement quantity by 1
decrease() => voidDecrement quantity by 1
setQuantity(qty: number) => voidSet specific quantity
inputPropsobjectProps 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


Support us


EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.