React Cart Development for 1C-Bitrix
The cart is not just a list of products. It is a critical component that directly determines conversion to order. The standard Bitrix cart works with a full page reload or through a legacy Ajax component. A React cart updates instantly, synchronizes with the backend without UI flashing, and works identically in a slide-out drawer and on the full checkout page.
Architecture: Cart as Shared State
The cart in a SPA is global state accessible from any component: the "Add to Cart" button on a product card, the header icon, the side drawer, the checkout page. All of them must display the same data.
// /src/store/cart.ts — Zustand store
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface CartItem {
id: number; // Bitrix cart item ID
productId: number;
name: string;
price: number;
quantity: number;
maxQuantity: number; // warehouse stock
image: string;
}
interface CartStore {
items: CartItem[];
total: number;
discount: number;
couponCode: string | null;
isLoading: boolean;
isOpen: boolean; // whether the side drawer is open
// Actions
fetchCart: () => Promise<void>;
addItem: (productId: number, quantity?: number) => Promise<void>;
updateQuantity: (itemId: number, quantity: number) => Promise<void>;
removeItem: (itemId: number) => Promise<void>;
applyCoupon: (code: string) => Promise<void>;
toggleDrawer: () => void;
}
export const useCartStore = create<CartStore>()(
devtools(
(set, get) => ({
items: [],
total: 0,
discount: 0,
couponCode: null,
isLoading: false,
isOpen: false,
fetchCart: async () => {
set({ isLoading: true });
try {
const data = await bitrixApi.get<CartData>('cart.get');
set({
items: data.items,
total: data.total,
discount: data.discount,
couponCode: data.coupon_code,
});
} finally {
set({ isLoading: false });
}
},
addItem: async (productId, quantity = 1) => {
// Optimistic update: show the change immediately
const prevItems = get().items;
const existing = prevItems.find(i => i.productId === productId);
if (existing) {
set(state => ({
items: state.items.map(i =>
i.productId === productId
? { ...i, quantity: i.quantity + quantity }
: i
),
}));
}
try {
const data = await bitrixApi.post<CartData>('cart.add', {
product_id: productId,
quantity,
});
set({ items: data.items, total: data.total, isOpen: true });
} catch (error) {
// Roll back optimistic update
set({ items: prevItems });
throw error;
}
},
updateQuantity: async (itemId, quantity) => {
if (quantity < 1) {
return get().removeItem(itemId);
}
const data = await bitrixApi.post<CartData>('cart.update', {
item_id: itemId,
quantity,
});
set({ items: data.items, total: data.total });
},
removeItem: async (itemId) => {
const data = await bitrixApi.post<CartData>('cart.remove', {
item_id: itemId,
});
set({ items: data.items, total: data.total });
},
applyCoupon: async (code) => {
const data = await bitrixApi.post<CartData>('cart.coupon', {
coupon: code,
});
set({ items: data.items, total: data.total, discount: data.discount,
couponCode: data.coupon_code });
},
toggleDrawer: () => set(s => ({ isOpen: !s.isOpen })),
})
)
);
PHP Backend for the Cart
Cart operations in Bitrix use the sale module:
// /local/ajax/api.php
CModule::IncludeModule('sale');
CModule::IncludeModule('catalog');
function getCartData(): array {
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
\Bitrix\Sale\Fuser::getId(), SITE_ID
);
$items = [];
foreach ($basket as $item) {
$items[] = [
'id' => $item->getId(),
'product_id' => $item->getProductId(),
'name' => $item->getField('NAME'),
'price' => $item->getPrice(),
'quantity' => $item->getQuantity(),
'max_quantity'=> getProductStock($item->getProductId()),
'image' => getProductImage($item->getProductId()),
];
}
return [
'items' => $items,
'total' => $basket->getPrice(),
'discount' => $basket->getBasePrice() - $basket->getPrice(),
'coupon_code'=> getAppliedCoupon(),
];
}
case 'cart.add':
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(...);
$item = \Bitrix\Sale\BasketItem::create($basket, 'catalog', (int)$_POST['product_id']);
$item->setFields([
'QUANTITY' => max(1, (int)$_POST['quantity']),
'CURRENCY' => \Bitrix\Currency\CurrencyManager::getBaseCurrency(),
'LID' => SITE_ID,
'PRODUCT_PROVIDER_CLASS' => '\CCatalogProductProvider',
]);
$basket->save();
echo json_encode(['result' => getCartData()]);
break;
Cart Component
// CartDrawer.tsx — side cart drawer
import { useCartStore } from '../store/cart';
export function CartDrawer() {
const { items, total, isOpen, toggleDrawer, updateQuantity, removeItem } = useCartStore();
return (
<aside className={`cart-drawer ${isOpen ? 'cart-drawer--open' : ''}`}>
<div className="cart-drawer__header">
<h2>Cart ({items.length})</h2>
<button onClick={toggleDrawer} aria-label="Close">✕</button>
</div>
<div className="cart-drawer__items">
{items.map(item => (
<CartItem
key={item.id}
item={item}
onQuantityChange={(q) => updateQuantity(item.id, q)}
onRemove={() => removeItem(item.id)}
/>
))}
</div>
<div className="cart-drawer__footer">
<div className="cart-total">Total: {formatPrice(total)}</div>
<a href="/order/" className="btn btn-primary btn-full">
Proceed to Checkout
</a>
</div>
</aside>
);
}
Synchronization with the Server Cart
The Bitrix cart is stored server-side (linked to an fUser — the anonymous user before login). On login, the anonymous cart must merge with the user's cart — Bitrix handles this automatically in the OnUserLoginExternal event handler.
Session restoration synchronization (user opens a new tab):
// On application mount
useEffect(() => {
useCartStore.getState().fetchCart();
}, []);
// When the user returns to the tab
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
useCartStore.getState().fetchCart();
}
});
A React cart with optimistic updates gives the UI instant responsiveness and hides network latency from the user. Combined with a correct PHP backend on top of the Bitrix cart, this solution is reliable and requires no rewriting of order logic — Bitrix still controls the entire sales process.







