React 19: useOptimistic + IndexedDB для offline-first синхронизации
-

React 19 приносит useOptimistic - хук, который делает optimistic UI нативным и без боли. С ним UI обновляется мгновенно, а синхронизация с сервером уходит в фон. Это киллер-фича для apps, где юзеры жмут кнопки оффлайн и не хотят ждать.
Комбинируем с IndexedDB и получаем полноценную offline-first стратегию. Данные пишутся локально, очередь синка держится в БД, а при коннекте всё улетает на сервер. Никаких лоадеров, никаких ‘подождите’ - чистый DX и топ Web Vitals.
useOptimistic: мгновенные обновления без бойлерплейта
useOptimistic - это хук, который держит два стейта: базовый (с сервера) и optimistic (для моментального фидбека). Ты кидаешь обновление в optimistic слой, UI рендерится сразу, а реальный запрос уходит асинхронно. Если сервер ответил ок - база обновляется. Если фейл - откатываемся или реконсилируем.
Представь туду-лист: юзер добавляет задачу оффлайн, список растет на глазах. В фоне данные ложатся в IndexedDB, optimistic стейт сияет. Сервер подтвердит - синк завершён, нет - конфликт решим last-write-wins. Это убирает тонну useState + useEffect + лоадеры.
Ключевые плюсы:
- Мгновенный рендер: UI не блокируется, CLS и FID на высоте.
- Нативный роллбэк: React сам мерджит стейты, без ручного трэка.
- Простота: один хук вместо кастомного стейт-менеджера.
Сценарий Без useOptimistic С useOptimistic Add task оффлайн Локер + очередь Мгновенный UI + DB Синк при коннекте Ручной retry Авто-мердж Конфликт Самописный resolver Last-write-wins IndexedDB как локальный source of truth
IndexedDB - это встроенная NoSQL БД в браузере, идеал для offline-first. Не путай с localStorage - тут транзакции, индексы, большой объём. Либы вроде dexie или idb упрощают API до уровня Firebase: openDB, add, getAll - и готово.
В offline-first всегда читаем из IndexedDB первым. Сеть только для апдейта локалки. Пишем очередь операций в отдельный стор syncQueue с UUID, timestamp и status. Оффлайн-сущности генерят clientId, чтоб не ждать серверных ID. При коннекте worker или хук прогоняет очередь, обновляя optimistic стейт.
Настройка базовая:
import { openDB } from 'idb'; const DB_NAME = 'offlineAppDB'; const STORE_NAME = 'tasks'; export const initDB = async () => { return openDB(DB_NAME, 1, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'clientId' }); } if (!db.objectStoreNames.contains('syncQueue')) { db.createObjectStore('syncQueue', { keyPath: 'id', autoIncrement: true }); } }, }); }; export const queueSync = async (operation: any) => { const db = await initDB(); await db.add('syncQueue', { ...operation, status: 'pending' }); };Бест-практисы:
- UUID для оффлайн-ID (не полагайся на сервер).
- Версионинг: track updatedAt для конфликтов.
- Queue survives restarts - IndexedDB persistent.
Синхронизация: от optimistic к серверу
Сердце offline-first - background sync. useOptimistic апдейтит UI, IndexedDB ловит данные, а синк-логик проверяет navigator.onLine и прогоняет очередь. Используй useEffect с event listener на ‘online’, или Service Worker для надёжности.
Пример туду-хука:
import { useOptimistic, useEffect } from 'react'; function useOfflineTodos(initialTodos: Todo[]) { const [optimisticTodos, addOptimisticTodo] = useOptimistic(initialTodos, (state, newTodo: Todo) => [...state, newTodo]); const addTodo = (todo: Todo) => { addOptimisticTodo({ ...todo, clientId: crypto.randomUUID(), status: 'pending' }); queueSync({ type: 'add', payload: todo }); }; useEffect(() => { const sync = async () => { if (navigator.onLine) { // прогоняем syncQueue, апдейтим optimisticTodos } }; window.addEventListener('online', sync); return () => window.removeEventListener('online', sync); }, [optimisticTodos]); return { optimisticTodos, addTodo }; }Шаги синка:
- Pending ops из syncQueue -> API calls батчем.
- Сервер OK? Update local DB + optimistic state.
- Error? Retry с экспоненциальной задержкой или mark ‘failed’.
Status Action UI Effect pending API POST/PUT Песочинки/Spinner synced Mark done
Иконкаfailed Retry queue
️ WarninguseOptimistic + IndexedDB в реальном туду-листе
Склеиваем всё в компонент. useOptimistic держит стейт, кастом-хук - DB + sync. Оффлайн юзер добавляет/чекует тудушки - UI летает. Коннект - синк прозрачный, без ререндера всего списка.
Полный пример:
const TodoList = ({ initialTodos }: { initialTodos: Todo[] }) => { const { optimisticTodos, addTodo, toggleTodo } = useOfflineTodos(initialTodos); return ( <div> <input onKeyDown={(e) => e.key === 'Enter' && addTodo({ title: e.currentTarget.value })} /> <ul> {optimisticTodos.map(todo => ( <li key={todo.clientId}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.clientId)} /> {todo.title} {todo.status === 'pending' && '⏳'} </li> ))} </ul> </div> ); };Фичи в действии:
- Optimistic toggle: чекбокс меняется до API.
- IndexedDB как бэкап: refresh - данные на месте.
- Конфликты: серверная версия > локальной - мерджим.
Это даёт FCP <100ms даже оффлайн, LCP без задержек. Линтеры и TS happy - типы на строгом уровне.
Код, который хочется клонировать
Вместе useOptimistic и IndexedDB превращают рендер в шелк. Оффлайн больше не баг - это фича. Хочется копать глубже в conflict resolution или SW для пуши-нотификаций, но базис уже летает.
Масштабируй на реальные apps: чаты, шопинг-корзины, формы. DX на уровне Next.js 15+, бандл минимальный - никаких внешних либ для стейта. React 19 реально меняет игру.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.