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({
recipient: 'John Doe',
address_line_1: '123 Main St',
locality: 'New York',
administrative_area: 'US-NY',
postal_code: '10001',
country: 'US',
telephone: '+1 212 555 0100'
});
Address type is snake_case and country-dependentThe keys are the shared address columns: recipient (or given_name + family_name), organization, address_line_1..3, dependent_locality, locality, administrative_area, postal_code, sorting_code, country, telephone, extra. Which ones are required follows the country's address schema. Passing an unknown key (fullName, province) is rejected with unknown_field; a validation failure throws an Error whose errors property is the server's [{ field, code, message }] array, which the address form maps onto its inputs with form.setError.
addBillingAddress
Add billing address:
const { addBillingAddress } = useCartDispatch();
await addBillingAddress({
recipient: 'John Doe',
address_line_1: '123 Main St',
locality: 'New York',
administrative_area: 'US-NY',
postal_code: '10001',
country: 'US',
telephone: '+1 212 555 0100'
});
// A logged-in customer can reuse a saved address instead:
await addBillingAddress({ customerAddressUuid: savedAddress.uuid });
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',
administrativeArea: 'US-CA', // optional, a region key
locality: 'Cupertino', // optional
dependentLocality: undefined, // optional (wards, neighbourhoods)
postalCode: '95014' // optional
});
// The checkout derives these from the country's address schema: every
// geographic field of the schema (C, S, Z, D tokens) plus the country, and
// re-fetches only when one of them changes — never on a name or telephone edit.
// 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