Every incomplete order is lost money. The default bitrix:sale.order.ajax on jQuery takes 1–2 seconds to update fields, and with 10,000+ items, it can be up to 3 seconds. The user leaves, conversion drops. Customizing a multi-step form, B2B requisites, or delivery date selection on this component is nearly impossible — the code turns into spaghetti of PHP inserts and JavaScript.
We replace sale.order.ajax with a React checkout, turnkey. Our engineers are certified Bitrix developers with 10+ years of experience. After implementation for one client, conversion increased from 62% to 79% within 6 weeks. If you want similar results, contact us — we'll evaluate your project.
Why a React checkout is faster and more flexible
React checkout solves the problem at the architecture level: UI in components, logic in hooks, communication with the server via API. Changing delivery is handled in 200 ms, while the standard component takes over a second. Field validation occurs instantly on blur — the user sees an error immediately, not after clicking the submit button.
How the React checkout architecture works
The checkout is split into two independent layers: the UI layer (React) and business logic (Bitrix on the server). On the front end, a React app manages the form, shows/hides steps, calculates totals in real time. On the server, Bitrix processes the order via \Bitrix\Sale\Order, applies discounts, calculates delivery costs, and checks stock. More about the order system in Bitrix Sale documentation.
The key API method for calculating an order without saving 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 parameters $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 total 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: choosing a delivery service, entering a promo code, changing quantity. React gets the updated figures without a page reload.
How to integrate React checkout with Bitrix
For a complex checkout (3+ steps with validation), we recommend React Hook Form with Zod schemas for validation:
const checkoutSchema = z.object({ contact: z.object({ name: z.string().min(2, 'Enter name'), 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 managed with Zustand: steps, current step, data per step, calculation result. When moving between steps, data is preserved and the user can go back.
We use React Hook Form for validation, Zustand for state, and React Query for queries.
| Technology | Usage |
|---|---|
| React Hook Form + Zod | Form validation with minimal re-renders |
| Zustand | Lightweight state manager for steps and UI states |
| React Query | Caching Bitrix API requests, automatic retry on failures |
Integration with maps for courier delivery
Yandex Maps or DaData for address autocomplete is a standard task for React checkout.
// 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 further order processing and handoff to delivery services.
Case study: checkout for a furniture retailer
Our client: an online furniture store with 2000+ SKUs. Specifics: items with different production times (3 to 40 days), option to choose a delivery date, mandatory measuring for some items, B2B checkout with legal entity details. The standard sale.order.ajax did not support delivery date selection, conditional display of the measuring block, or company details in a single flow.
Implementation:
- Step 1 — Contact. A form with phone and name. Phone validated via libphonenumber-js, SMS verification optional.
-
Step 2 — Delivery. Dynamic display: if the order contains items requiring measuring, a "Schedule measuring" block appears with a datepicker. Available dates are loaded from the server (from Bitrix CRM, occupied slots are blocked). Delivery date selection accounts for production time — the minimum date is calculated server-side as
max(PRODUCTION_DAYS)in the cart. -
Step 3 — Payment. A toggle "Individual / Legal entity". When legal entity is selected, a block with company details expands (INN → autocomplete via DaData → pulls KPP, name, address). A non-cash invoice for B2B is automatically generated after order creation via
\Bitrix\Sale\PaySystem\Manager. - Order creation. A final POST sends all data to the server. Bitrix creates the order, attaches custom properties (delivery date, client type, company details), sends notifications. React receives the order ID and redirects to the "Thank you" page.
| Feature | Standard Bitrix | React checkout |
|---|---|---|
| Delivery date selection | Impossible | Datepicker with busy slots |
| B2B details | Separate form | Inline, same flow |
| Real-time validation | Only on submit | Instant, on blur |
| Total update on delivery change | Block reload (>1 s) | No reload (<200 ms) |
Conversion increased from 62% to 79% within 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, INN, 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 during checkout is handled at the final save step. React shows a modal with a list of unavailable items and offers to remove them or save the order without them.
Connection loss during checkout — React Query with retry: 3 and notification to the user. Form data is saved in sessionStorage and restored on reload.
What's included in the work
- Designing checkout steps, conditional logic, validation
- Developing API controllers: order calculation, creation, fetching delivery services and pickup points
- Creating the React app: form, state manager, map/DaData integration
- Binding custom order properties, configuring payment systems
- Testing edge cases: empty cart, insufficient stock, session timeout
- API and code documentation, training your team
Project timelines and process
Our process: data collection → audit/analysis → design → estimate → development → testing → launch. Timelines range from two weeks for a basic checkout to two months for a complex multi-step solution with B2B features and map integration. The exact timeline is determined after analysis of your requirements.
Ready to speed up your checkout? Order React checkout development — contact us. Get a consultation for your project — we'll tell you exactly which steps are needed.

