Cart Context
Description
A React context that manages the shopping cart state and provides methods for cart operations like adding items, updating quantities, applying coupons, and managing checkout information.
Import
import { CartProvider, useCartState, useCartDispatch } from '@components/frontStore/cart/CartContext';
Setup
Wrap your application with the CartProvider:
import { CartProvider } from '@components/frontStore/cart/CartContext';
function App({ cart, query, addMineCartItemApi }) {
return (
<CartProvider
cart={cart}
query={query}
addMineCartItemApi={addMineCartItemApi}
>
{/* Your app components */}
</CartProvider>
);
}
Hooks
useCartState
Access cart state and data:
import { useCartState } from '@components/frontStore/cart/CartContext';
function CartSummary() {
const { data, loading, loadingStates } = useCartState();
return (
<div>
<p>Items: {data.totalQty}</p>
<p>Total: {data.grandTotal.text}</p>
{loading && <span>Loading...</span>}
</div>
);
}
useCartDispatch
Access cart operations:
import { useCartDispatch } from '@components/frontStore/cart/CartContext';
function AddToCartButton({ sku }) {
const { addItem } = useCartDispatch();
const handleAdd = async () => {
await addItem({ sku, qty: 1 });
};
return <button onClick={handleAdd}>Add to Cart</button>;
}
Cart State Structure
interface CartState {
data: CartData; // Cart data
loading: boolean; // Overall loading state
loadingStates: { // Specific loading states
addingItem: boolean;
removingItem: string | null;
updatingItem: string | null;
addingPaymentMethod: boolean;
addingShippingMethod: boolean;
addingShippingAddress: boolean;
addingBillingAddress: boolean;
addingContactInfo: boolean;
applyingCoupon: boolean;
removingCoupon: boolean;
fetchingShippingMethods: boolean;
};
syncStatus: {
syncing: boolean; // Sync in progress
synced: boolean; // Last sync successful
trigger?: string; // What triggered the sync
};
}
Cart Operations
addItem
Add a product to the cart:
const { addItem } = useCartDispatch();
await addItem({ sku: 'PROD-123', qty: 2 });
removeItem
Remove an item from the cart:
const { removeItem } = useCartDispatch();
const { data } = useCartState();
const handleRemove = async (itemId) => {
await removeItem(itemId);
};
updateItem
Update item quantity:
const { updateItem } = useCartDispatch();
// Increase quantity
await updateItem(itemId, { qty: 1, action: 'increase' });
// Decrease quantity
await updateItem(itemId, { qty: 1, action: 'decrease' });
addShippingAddress
Add shipping address:
const { addShippingAddress } = useCartDispatch();
await addShippingAddress({
full_name: 'John Doe',
address_1: '123 Main St',
city: 'New York',
province: 'NY',
postcode: '10001',
country: 'US',
telephone: '555-0100'
});
Address type is snake_caseThe keys are full_name, address_1, address_2, city, province, postcode,
country, telephone. Passing camelCase (fullName, address1) does not silently
save the wrong shape — validateAddress rejects the call with Full name is required.
addBillingAddress
Add billing address:
const { addBillingAddress } = useCartDispatch();
await addBillingAddress({
full_name: 'John Doe',
address_1: '123 Main St',
city: 'New York',
province: 'NY',
postcode: '10001',
country: 'US',
telephone: '555-0100'
});
addPaymentMethod
Select payment method:
const { addPaymentMethod } = useCartDispatch();
await addPaymentMethod('stripe', 'Credit Card');
addShippingMethod
Select a shipping method. The signature is addShippingMethod(code, name, providerCode) — all three arguments are required:
const { data } = useCartState();
const { addShippingMethod } = useCartDispatch();
// Always thread the values straight off `availableShippingMethods`.
const method = data.availableShippingMethods[0];
await addShippingMethod(method.code, method.name, method.providerCode);
providerCode is requiredaddShippingMethod throws Cannot add shipping method: providerCode is required when the third argument is missing or empty — there is no silent fallback to 'core'.
providerCode identifies which shipping provider the method came from (core, USPS, EasyPost, …). The server uses it to route validation to that provider's validateMethod; passing the wrong one makes the selection fail with "method no longer available". Read it from availableShippingMethods[].providerCode — the GraphQL field is non-nullable (String!), so it is always present.
addContactInfo
Add customer contact information:
const { addContactInfo } = useCartDispatch();
await addContactInfo({ email: 'customer@example.com' });
applyCoupon
Apply a discount coupon:
const { applyCoupon } = useCartDispatch();
await applyCoupon('SUMMER20');
removeCoupon
Remove applied coupon:
const { removeCoupon } = useCartDispatch();
await removeCoupon();
fetchAvailableShippingMethods
Fetch shipping methods for an address. Signature: fetchAvailableShippingMethods(params: ShippingAddressParams).
const { fetchAvailableShippingMethods } = useCartDispatch();
const { data } = useCartState();
await fetchAvailableShippingMethods({
country: 'US',
province: 'CA', // optional
postcode: '90210' // optional
});
// The result is written back onto cart state:
data.availableShippingMethods.forEach((method) => {
console.log(method.providerCode, method.code, method.name, method.cost?.text);
});
It runs a GraphQL query, writes the result to data.availableShippingMethods, and toggles loadingStates.fetchingShippingMethods. Each entry has this shape:
interface AvailableShippingMethod {
providerCode: string; // required — pass through to addShippingMethod
code: string;
name: string;
cost?: {
value: number;
text: string;
};
}
Throws Cannot fetch shipping methods: cart not initialized when the cart has no UUID yet.
Helper Methods
isShippingRequired
Check if cart requires shipping:
const { isShippingRequired } = useCartDispatch();
if (isShippingRequired()) {
// Show shipping form
}
isReadyForCheckout
Check if cart is ready for checkout:
const { isReadyForCheckout } = useCartDispatch();
const canCheckout = isReadyForCheckout();
getErrors
Get validation errors:
const { getErrors } = useCartDispatch();
const errors = getErrors();
errors.forEach(error => {
console.log(error.message);
});
getId
Get cart UUID:
const { getId } = useCartDispatch();
const cartId = getId();
clearError
Clear error state:
const { clearError } = useCartDispatch();
clearError();
Complete Example
import { useCartState, useCartDispatch } from '@components/frontStore/cart/CartContext';
function CartPage() {
const { data, loading, loadingStates } = useCartState();
const { removeItem, updateItem, applyCoupon } = useCartDispatch();
const handleRemove = async (itemId) => {
try {
await removeItem(itemId);
} catch (error) {
console.error('Failed to remove item:', error);
}
};
const handleUpdateQty = async (itemId, action) => {
try {
await updateItem(itemId, { qty: 1, action });
} catch (error) {
console.error('Failed to update quantity:', error);
}
};
return (
<div>
<h1>Shopping Cart</h1>
{loading && <div>Loading...</div>}
{data.items.map(item => (
<div key={item.cartItemId}>
<h3>{item.productName}</h3>
<p>Price: {item.productPrice.text}</p>
<p>Qty: {item.qty}</p>
<button
onClick={() => handleUpdateQty(item.cartItemId, 'increase')}
disabled={loadingStates.updatingItem === item.cartItemId}
>
+
</button>
<button
onClick={() => handleUpdateQty(item.cartItemId, 'decrease')}
disabled={loadingStates.updatingItem === item.cartItemId}
>
-
</button>
<button
onClick={() => handleRemove(item.cartItemId)}
disabled={loadingStates.removingItem === item.cartItemId}
>
Remove
</button>
</div>
))}
<div>
<h3>Summary</h3>
<p>Subtotal: {data.subTotal.text}</p>
<p>Shipping: {data.shippingFeeExclTax.text}</p>
<p>Tax: {data.taxAmount.text}</p>
<p>Total: {data.grandTotal.text}</p>
</div>
</div>
);
}
Loading States
Use specific loading states for better UX:
const { loadingStates } = useCartState();
// Check if adding item
if (loadingStates.addingItem) {
// Show loading spinner
}
// Check if specific item is being removed
if (loadingStates.removingItem === itemId) {
// Show loading on that item
}
// Check if updating specific item
if (loadingStates.updatingItem === itemId) {
// Show loading on that item
}
Error Handling
const { data } = useCartState();
const { clearError } = useCartDispatch();
useEffect(() => {
if (data.error) {
alert(data.error);
clearError();
}
}, [data.error]);
Sync Status
Monitor cart synchronization with server:
const { syncStatus } = useCartState();
if (syncStatus.syncing) {
// Show syncing indicator
}
if (syncStatus.synced) {
// Show success message
}
// Check what triggered the sync
console.log(syncStatus.trigger); // e.g., 'addItem', 'applyCoupon'
Features
- State Management: Centralized cart state with React Context
- Loading States: Granular loading states for each operation
- Error Handling: Built-in error management
- Retry Logic: Automatic retry for failed requests
- Sync Status: Track synchronization with server
- Type Safe: Full TypeScript support
- Extensible: Support for custom fields via extensions
- GraphQL Integration: Uses urql for data fetching
Related Components
- AppProvider - App context provider
- useAppState - App state hook