Developing a React Shopping Cart for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    745
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.