React Checkout Development for 1C-Bitrix
Checkout is the most vulnerable point in the funnel. The standard Bitrix bitrix:sale.order.ajax component runs on jQuery and a template system, updating page sections through AJAX HTML-block replacement. That worked in 2015. Today it is difficult to customize, slow to render, and scales poorly for non-standard scenarios: multi-step checkout with conditional logic, delivery to multiple addresses, B2B fields (company details, tax IDs), map integration.
A React checkout addresses the problem at the architecture level: all UI lives in components, logic is concentrated in hooks and a state manager, and server communication goes through a clean API.
React Checkout Architecture
The checkout is split into two independent layers: the UI layer (React) and business logic (Bitrix on the server).
On the frontend — a React application that manages the form, shows/hides steps, and calculates totals in real time. On the server — Bitrix processes the order via \Bitrix\Sale\Order, applies discounts, calculates delivery costs, and checks stock.
The key API method for calculating the order without creating it:
// Calculate totals without saving the order
$order = \Bitrix\Sale\Order::create(SITE_ID, $userId);
$basket = \Bitrix\Sale\Basket::loadSiteBasket(SITE_ID);
$order->setBasket($basket);
// Apply delivery settings
$shipment = $order->getShipmentCollection()->createItem(
\Bitrix\Sale\Delivery\Services\Manager::getById($deliveryId)
);
$shipment->setFields(['DELIVERY_ID' => $deliveryId, 'CURRENCY' => 'RUB']);
$shipment->calculateDelivery();
// Apply coupon
$order->getDiscountSystem()->calculate();
// Return totals without saving (no $order->save() call)
return [
'subtotal' => $basket->getPrice(),
'delivery_price' => $shipment->getPrice(),
'discount' => $order->getDiscountPrice(),
'total' => $order->getPrice(),
];
This endpoint is called on every field change: selecting a delivery service, entering a promo code, changing a quantity. React receives up-to-date figures without a page reload.
Multi-Step Form and State Management
For a complex checkout (3+ steps with validation), React Hook Form with Zod validation schemas is the optimal choice:
const checkoutSchema = z.object({
contact: z.object({
name: z.string().min(2, 'Name is required'),
phone: z.string().regex(/^\+7\d{10}$/, 'Invalid format'),
email: z.string().email('Invalid email'),
}),
delivery: z.object({
type: z.enum(['courier', 'pickup', 'cdek']),
address: z.string().optional(),
pickupId: z.number().optional(),
}),
payment: z.object({
method: z.enum(['online', 'cash', 'invoice']),
}),
});
Checkout state is stored in Zustand: steps, current step, data for each step, calculation result. Data is not lost when moving between steps; the user can go back.
Map Integration for Courier Delivery
Yandex Maps or DaData for address autocomplete is a standard checkout requirement in React.
// Hook for address autocomplete via DaData
function useAddressSuggest(query: string) {
return useQuery({
queryKey: ['address-suggest', query],
queryFn: () => fetchDaDataSuggestions(query),
enabled: query.length > 3,
staleTime: 60_000,
});
}
When an address is selected via DaData, structured data (city, street, postal code) is passed to Bitrix as separate fields — this simplifies subsequent order processing and handoff to delivery services.
Case Study: Furniture Retailer Checkout
Online furniture store. Specifics: products with different production lead times, ability to book delivery for a specific date, mandatory measurement service for certain products, B2B checkout with company details. The standard sale.order.ajax supported none of these: no delivery date selection, no conditional measurement block, no company details in a single flow.
Implementation:
-
Step 1 — Contact details. Form with phone and name. Phone validated with libphonenumber-js; SMS verification as an optional enhancement.
-
Step 2 — Delivery. Dynamic display: if the order contains products requiring measurement, a "Schedule Measurement" block with a datepicker appears. Available dates are loaded from the server (from Bitrix CRM; occupied slots are closed). Delivery date selection accounts for production lead time — the minimum date is calculated server-side using
max(PRODUCTION_DAYS)across cart items. -
Step 3 — Payment. A toggle between "Individual" and "Business." Selecting "Business" expands a company details block (tax ID → autocomplete via DaData → populating registration number, company name, address). An invoice for B2B is generated automatically after order creation via
\Bitrix\Sale\PaySystem\Manager. -
Order creation. The final POST sends all data to the server. Bitrix creates the order, attaches custom properties (delivery date, client type, company details), and sends notifications. React receives the order ID and redirects the user to the "Thank You" page.
| Step | Standard Bitrix | React Checkout |
|---|---|---|
| Delivery date selection | Not possible | Datepicker with occupied slots |
| B2B company details | Separate form | Inline, in the same flow |
| Real-time validation | On submit only | Instant, on blur |
| Totals recalculation on delivery change | Block reload | No reload, <200 ms |
Checkout conversion increased from 62% to 79% in the first 6 weeks after launch.
Server-Side Order Creation
public function createOrderAction(array $data): array
{
$order = \Bitrix\Sale\Order::create(SITE_ID, $this->getCurrentUserId());
$basket = \Bitrix\Sale\Basket::loadSiteBasket(SITE_ID);
$order->setBasket($basket);
// Contact
$order->setField('USER_DESCRIPTION', $data['comment'] ?? '');
// Delivery
$shipmentCollection = $order->getShipmentCollection();
$shipment = $shipmentCollection->createItem(
\Bitrix\Sale\Delivery\Services\Manager::getById($data['delivery_id'])
);
$shipment->setField('DELIVERY_ID', $data['delivery_id']);
// Payment
$paymentCollection = $order->getPaymentCollection();
$payment = $paymentCollection->createItem(
\Bitrix\Sale\PaySystem\Manager::getObjectById($data['payment_id'])
);
$payment->setField('PAY_SYSTEM_ID', $data['payment_id']);
$payment->setField('SUM', $order->getPrice());
// Order properties (address, phone, tax ID, etc.)
$propertyCollection = $order->getPropertyCollection();
foreach ($data['properties'] as $code => $value) {
$prop = $propertyCollection->getItemByOrderPropertyCode($code);
if ($prop) {
$prop->setValue($value);
}
}
$result = $order->save();
if (!$result->isSuccess()) {
throw new \Exception(implode(', ', $result->getErrorMessages()));
}
return ['order_id' => $order->getId()];
}
Error Handling and Edge Cases
Insufficient stock at checkout — handled at the final save step. React shows a modal listing unavailable items and offers to remove them or save the order without them.
Connection lost during checkout — React Query with retry: 3 and a user notification. Form data is saved to sessionStorage and restored on page reload.
Scope of Work
- Designing checkout steps, conditional logic, and validation rules
- API controllers: order calculation, order creation, delivery services and pickup points
- React application development: form, state, map/DaData integration
- Order creation in Bitrix with the full set of fields, properties, and payment systems
- Edge case testing: empty cart, insufficient stock, session timeout







