React 19: useOptimistic для мгновенной корзины e-commerce с откатом ошибок
-

useOptimistic в React 19 - это киллер-фича для отзывчивых UI. Представь: юзер добавляет товар в корзину, и она обновляется мгновенно, без лагов от сетевых запросов. А если сервер отвалился - стейт откатывается автоматически. Идеально для e-commerce, где каждая миллисекунда на счету.
Эта тема спасет от типичных болей: спиннеры на пол-экрана, фрустрация юзеров и убитые Core Web Vitals. Разберем на примере корзины - от базового сетапа до обработки ошибок. Код на TS, минимум бойлерплейта, максимум DX.
Как работает useOptimistic под капотом
useOptimistic берет твое реальное состояние и возвращает оптимистичную копию. Ты даешь ему редьюсер-функцию, которая говорит: “а что если действие сработает? Покажи это в UI”. Пока запрос висит, рендерится оптимистичный стейт. Сервер ответил - реальное состояние обновляется, и optimistic синхронизируется.
В e-commerce это магия: добавь товар - корзина сразу показывает +1 итем, даже если API тормозит. Ключевой нюанс: хук завязан на реальный стейт как на source of truth. Ошибка? Реальный стейт не меняется - optimistic откатывается сам. Никаких ручных if’ов и try-catch хеллов.
Это упрощает жизнь по сравнению с ручными optimistic updates в useState + useTransition. Вот базовая схема:
- Первый аргумент: текущее реальное состояние (массив товаров корзины).
- Второй аргумент: редьюсер
(state, action) => optimisticState- здесь action это данные для обновления (новый товар). - Возврат:
[optimisticState, updateFn]- рендерим optimisticState, вызываем updateFn перед запросом.
Сравнение подходов useState + manual useOptimistic Код 20+ строк с loading/error 5 строк чистоты Откат ошибок Ручной Автоматический DX Боль Кайф Реальный пример: корзина с мгновенным добавлением
Начнем с типичной корзины. У нас массив товаров, API-эндпоинт для добавления. Без useOptimistic юзер кликает “Добавить” - спиннер, 500мс лага, CLS прыгает. С хуком - товар появляется instantly, с меткой “Отправка…”.
Код супер-изящный. Смотри, как это выглядит в компоненте Cart:
import { useOptimistic } from 'react'; type CartItem = { id: string; name: string; qty: number }; type CartState = CartItem[]; const addToCart = (state: CartState, newItem: CartItem): CartState => [ ...state, { ...newItem, qty: 1, status: 'pending' as const } ]; function Cart({ realCart }: { realCart: CartState }) { const [optimisticCart, addOptimisticItem] = useOptimistic(realCart, addToCart); const handleAdd = async (item: Omit<CartItem, 'qty' | 'status'>) => { const optimisticItem = { ...item, qty: 1, status: 'pending' }; addOptimisticItem(optimisticItem); try { await fetch('/api/cart', { method: 'POST', body: JSON.stringify(item) }); // Реальный стейт обновится через revalidation (Next.js) или внешний setter } catch { // Ничего не делаем - optimistic откатится сам } }; return ( <ul> {optimisticCart.map(item => ( <li key={item.id}> {item.name} (x{item.qty}) {item.status === 'pending' && ' ➤ Отправка...'} </li> ))} </ul> ); }Видишь, как лаконично? handleAdd вызывает addOptimisticItem до fetch. UI мигнул - товар в списке. Сервер OK - реальный стейт апдейтится (через SWR/mutate или Next.js revalidate). Ошибка - optimisticCart синхронизируется с realCart автоматически.
Плюсы этого подхода:
- Мгновенный фидбек - LCP/FCP на высоте.
- Автооткат - никаких race conditions.
- Suspense-ready - работает с React 19 новыми фичами вроде use.
Обработка ошибок и edge-кейсов
Сетевые фейлы - классика e-commerce. useOptimistic бьет их одним выстрелом. Когда fetch падает, просто не трогай реальный стейт. Хук заметит расхождение и откатит optimistic к реальному.
Более хитрый кейс: обновление количества. Юзер меняет qty с 1 на 5 - пока API думает, показываем 5. Ошибка? Откат к 1. Добавь статусы для granular контроля:
type ItemStatus = 'idle' | 'pending' | 'error'; const updateQty = (state: CartState, { id, qty }: { id: string; qty: number }) => state.map(item => item.id === id ? { ...item, qty, status: 'pending' } : item );Нюансы, которые спасут нервы:
- Идемпотентность: делай action-данные уникальными (timestamp/id), чтоб избежать дублей.
- Batch updates: несколько addToCart подряд? Хук их merge’ит умно.
- Server errors: в редьюсере можно return fallback-state с уведомлением.
Кейс Без хука С useOptimistic Add item + network fail Спиннер вечно Мгновенно + откат Update qty Ручной loading Авто Offline Полный ступор Оптимистично до reconnect Улучшение UX: статусы, удаление, оффлайн
Расширим корзину. Добавим удаление и оффлайн-поддержку. useOptimistic идеален для этого - редьюсер может handle’ить разные action types.
const cartReducer = (state: CartState, action: CartAction): CartState => { switch (action.type) { case 'ADD': return [...state, { ...action.payload, status: 'pending' }]; case 'REMOVE': return state.filter(item => item.id !== action.id).map(item => ({ ...item, status: 'pending' })); default: return state; } };Теперь один хук - все операции. Оффлайн? Пока нет сети, UI отзывчивый. Reconnect - синхронизируется. Web Vitals взлетают: CLS=0, FID<10мс.
- Удаление: миг - итем исчез, “Удаление…”. OK/откат.
- Оффлайн-first: optimistic держит стейт, sync при online.
- Accessibility: добавь aria-live для скринеров.
useOptimistic + Next.js 15: прод-готовый стек
В Next.js это вообще песня. С React Server Actions + revalidatePath/Promise хук интегрируется из коробки. App Router? Server Components фетчат realCart, клиентские - optimistic.
'use server'; export async function addToCartServer(item: CartItem) { // Server logic revalidatePath('/cart'); }Клиент вызывает action, optimistic апдейт - DX на уровне. Производительность: SSR для initial, CSR optimistic для интерактива. Бандл минимальный, нет лишних useEffect.
Почему это меняет e-commerce игру
useOptimistic - не просто хук, а паттерн для future-proof UI. Корзина ощущается native-приложением: мгновенно, надежно, с грациозным откатом. Забудь про loading skeletons везде.
Осталось доработать: интеграция с Zustand/Jotai для global state, тесты с MSW, A/B-тесты метрик. Подумай, как замиксовать с useFormStatus для форм чекаута - выйдет монстр отзывчивости.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.