Розроблення однострашневого Checkout для інтернет-магазину
Однострашневий checkout розміщує всі етапи оформлення заказу на одному екрані — без переходів між кроками. Це зменшує кількість HTTP-запитів, усуває втрату даних при навігації браузера та скорочує сприймане час оформлення. В середньому конверсія вища на 10–25% порівняно з багатокроковим варіантом на мобільних пристроях. Розроблення займає 5–8 робочих днів.
Компонування екрана
Стандартний layout однострашневого checkout:
┌─────────────────────────────┬──────────────────────┐
│ Контакти │ │
│ Адреса доставки │ Склад заказу │
│ Способ доставки │ Промокод │
│ Способ оплати │ Итогова сума │
│ [Оформити заказ] │ │
└─────────────────────────────┴──────────────────────┘
На мобільних — одна колонка, права панель з итогом прокручується вверх після складу заказу. Sticky-кнопка «Оформити» фіксується внизу екрана.
Динамічне оновлення правої панелі
Права панель перераховується при кожній зміні: вибір методу доставки змінює итогову суму, введення промокода — скидку. Використовуємо debounced реактивність:
const { shipping, coupon } = useCheckoutStore();
const { data: summary } = useQuery({
queryKey: ['checkout-summary', shipping?.id, coupon],
queryFn: () => api.post('/checkout/summary', { shipping_id: shipping?.id, coupon }),
staleTime: 30_000,
enabled: !!shipping,
});
Запит до сервера йде тільки при реальних змінах, не на кожен keystroke у полі адреси.
Умовна видимість полів
Поля оплати та доставки з'являються по мірі заповнення попередніх блоків. Логіка управління станом форми:
const { watch } = useFormContext();
const email = watch('contact.email');
const addressFilled = watch(['address.city', 'address.street', 'address.house'])
.every(Boolean);
return (
<>
<ContactBlock />
{email && <AddressBlock />}
{addressFilled && <ShippingBlock />}
{selectedShipping && <PaymentBlock />}
</>
);
Такий підхід зменшує когнітивну навантаження — користувач не видит весь екран одразу, а заповнює послідовно.
Валідація без блокування
В однострашневому checkout важливо не блокувати кнопку «Оформити» до повного заповнення форми — це створює ощущення тупика. Замість цього:
- Поля валідуються при
onBlur, неonChange - Кнопка завжди активна
- При кліці срабатує
trigger()з React Hook Form, підсвічуються всі незаповнені поля та сторінка скроллится до першої помилки
const handleSubmit = async () => {
const valid = await form.trigger();
if (!valid) {
const firstError = Object.keys(form.formState.errors)[0];
document.querySelector(`[name="${firstError}"]`)?.scrollIntoView({ behavior: 'smooth' });
return;
}
await placeOrder(form.getValues());
};
Автозаповнення з профілю
Для авторизованих користувачів форма передзаповнюється даними з профілю:
useEffect(() => {
if (user) {
form.reset({
contact: { email: user.email, phone: user.phone },
address: user.defaultAddress ?? {},
});
}
}, [user]);
Якщо у користувача кілька збережених адрес — показуємо dropdown «Вибрати збережену адресу», при виборі підставляємо поля.
Inline-вибір доставки
Методи доставки відображаються карточками з іконкою перевізника, ціною та строком. При переключенні між методами права панель миттєво оновляє итог. Якщо адреса ще не введена — показуємо skeleton-заглушки з «від X ₽».
<RadioGroup value={selectedShipping?.id} onValueChange={handleShippingChange}>
{shippingOptions.map(option => (
<RadioGroupItem key={option.id} value={option.id}>
<span>{option.carrier_name}</span>
<span>{option.estimated_days} дн.</span>
<span className="font-bold">{option.price === 0 ? 'Бесплатно' : `${option.price} ₽`}</span>
</RadioGroupItem>
))}
</RadioGroup>
Вбудовані платіжні форми
Для банківських карт використовуємо JS SDK платіжного провайдера (ЮKassa, Tinkoff, CloudPayments). Форма карти — iframe провайдера прямо всередині checkout, без редіректу:
const widget = new cp.CloudPayments();
widget.pay('charge', {
publicId: PUBLIC_ID,
amount: summary.total,
currency: 'RUB',
invoiceId: order.id,
email: form.getValues('contact.email'),
}, {
onSuccess: (options) => router.push(`/orders/${order.id}/confirmation`),
onFail: (reason) => toast.error(`Платіж не пройшов: ${reason}`),
});
Продуктивність
Однострашневий checkout важить більше багатокрокового — всі блоки монтуються одразу. Оптимізації:
- Code splitting платіжного SDK — завантажується тільки при виборі методу «Карта»
- Lazy import компонентів карти, не потрібних при першому рендері
-
Preconnect до API DaData та платіжного провайдера в
<head>
Цільовий час до інтерактивності — менше 2 секунд на 4G.
Збереження прогресу
Дані форми зберігаються в sessionStorage через Zustand persist. Якщо користувач випадково закрив вкладку — при поверненні форма відновлюється. TTL сесії — 2 години, після цього чернетка видаляється.







