useOptimistic + useActionState: stale closure не прощает небрежной типизации
useOptimistic + useActionState: stale closure не прощает небрежной типизации React 19 принес useOptimistic в stable, но в связке с useActionState и flushSync легко нарваться на неочевидный баг с захватом устаревшего состояния. Проблема особенно остра в production-приложениях с глобальным стейтом (Zustand, Redux) и асинхронными мутациями. Как closure убивает оптимистичный UI Когда ты захватываешь состояние внутри useOptimistic, closure запоминает значение на момент вызова addOptimistic. Если action асинхронный, рендер пойдет по старым данным, а пользователь увидит рассинхрон. Типизация без явных дженериков не спасет: const [optimisticState, addOptimistic] = useOptimistic<State, Action>( state, (currentState, optimisticValue) => { // currentState уже устарел к моменту выполнения return currentState; } ); Решение: useRef + flushSync для синхронной пачки Сохраняй актуальное состояние в useRef и используй flushSync, чтобы optimistic update и реальный action выполнились в одной синхронной пачке без race condition: const stateRef = useRef(state); stateRef.current = state; const [optimisticState, addOptimistic] = useOptimistic<State, Action>( state, (currentState, action) => { const freshState = stateRef.current; return { ...freshState, version: freshState.version + 1, items: action === 'add' ? [...freshState.items, 'new'] : freshState.items.slice(0, -1) }; } ); function safeAction() { flushSync(() => { addOptimistic('add'); formAction('add'); // реальная мутация }); } Типичные ошибки и советы * Типизируй useOptimistic явно через <State, Action> — иначе TypeScript подсунет any, и ошибка станет runtime. * Не игнорируй useRef: прямое замыкание на state в асинхронном коде — классика багов, которые воспроизводятся рандомно. * flushSync тут не просто оптимизация: он гарантирует детерминированный порядок операций без race condition между optimistic update и action. Trade-off: синхронная пачка может блокировать рендер, если мутация тяжелая. Измеряй время выполнения action, чтобы не убить производительность. Вывод: useOptimistic спасает UX при серверных мутациях, но без детерминированной обработки stale closure через useRef и flushSync интерфейс будет глючить в production.