Розробка кошика на React для 1С-Бітрікс
Кошик — це не просто список товарів. Це критично важливий компонент, від якого безпосередньо залежить конверсія в замовлення. Стандартний кошик Бітрікс працює з повним перезавантаженням сторінки або через застарілий Ajax-компонент. React-кошик оновлюється миттєво, синхронізується з бекендом без мигання інтерфейсу і працює однаково у спливаючій панелі та на повній сторінці оформлення замовлення.
Архітектура: кошик як спільний стан
Кошик у SPA — це глобальний стан, доступний з будь-якого компонента: кнопка «До кошика» на картці товару, іконка в шапці, бокова панель кошика, сторінка оформлення. Всі вони мають показувати одні й ті самі дані.
// /src/store/cart.ts — Zustand store
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface CartItem {
id: number; // ID елемента кошика Бітрікс
productId: number;
name: string;
price: number;
quantity: number;
maxQuantity: number; // залишок на складі
image: string;
}
interface CartStore {
items: CartItem[];
total: number;
discount: number;
couponCode: string | null;
isLoading: boolean;
isOpen: boolean; // чи відкрита бокова панель
// Дії
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) => {
// Оптимістичне оновлення: показуємо зміну одразу
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) {
// Відкат оптимістичного оновлення
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-бекенд для кошика
Операції з кошиком у Бітрікс використовують модуль sale:
// /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;
Компонент кошика
// CartDrawer.tsx — бокова панель кошика
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>Кошик ({items.length})</h2>
<button onClick={toggleDrawer} aria-label="Закрити">✕</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">Разом: {formatPrice(total)}</div>
<a href="/order/" className="btn btn-primary btn-full">
Оформити замовлення
</a>
</div>
</aside>
);
}
Синхронізація із серверним кошиком
Кошик у Бітрікс зберігається на сервері (прив'язаний до fUser — анонімного користувача до авторизації). При авторизації анонімний кошик має об'єднатися з кошиком користувача — це робить Бітрікс автоматично в обробнику OnUserLoginExternal.
Синхронізація при відновленні сесії (користувач відкрив нову вкладку):
// При монтуванні застосунку
useEffect(() => {
useCartStore.getState().fetchCart();
}, []);
// При поверненні користувача на вкладку
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
useCartStore.getState().fetchCart();
}
});
React-кошик з оптимістичними оновленнями дає миттєвий відгук UI і приховує затримку мережевих запитів від користувача. У поєднанні з правильним PHP-бекендом на Бітрікс-кошику це рішення працює надійно і не потребує переписування логіки замовлень — Бітрікс як і раніше керує всім процесом продажу.







