useSyncExternalStore против ручных подписок в Concurrent React: как не допустить tearin...
useSyncExternalStore против ручных подписок в Concurrent React: как не допустить tearing UI В Concurrent React классическая проблема — tearing, или разрыв UI, при чтении внешнего стора во время рендера. Многие разработчики по инерции используют ручные подписки через useEffect, забывая, что в Concurrent Mode React может прервать рендер и затем продолжить с устаревшим состоянием. Проблема ручных подписок Ручная подписка выглядит так: function useStore(store) { const [state, setState] = useState(store.getState()); useEffect(() => { const unsub = store.subscribe(() => setState(store.getState())); return unsub; }, [store]); return state; } Типичная ошибка: между useState(store.getState()) и подпиской в useEffect может произойти параллельное обновление стора. В Concurrent Mode React перерисовывает UI в несколько проходов, и если стор изменился между ними — вы получите неконсистентный интерфейс. Особенно остро это проявляется на продакшене с тяжелыми дашбордами, например, при агрессивном кэшировании или WebSocket-стримах. Решение: useSyncExternalStore Начиная с React 18, используйте useSyncExternalStore: import { useSyncExternalStore } from 'react'; function useStore(store) { const state = useSyncExternalStore( store.subscribe, () => store.getState() ); return state; } Ключевой момент: getSnapshot вызывается синхронно при каждом рендере. Если снапшот изменился — React перезапускает рендер, гарантируя консистентность. Это закрывает гонки, которые сложно воспроизвести локально. Практический совет Если ваш проект на React 18+, ставьте useSyncExternalStore по умолчанию для всех внешних сторов (Redux, Zustand, MobX). Даже если сейчас вы не используете Concurrent Features, переход на них позже не сломает UI. Ложная абстракция здесь окупается надежностью. Предупреждение Не используйте ручные подписки в среде Concurrent React, если не уверены, что стор иммутабельно меняет всё дерево за один тик. На маленьких проектах tearing может не проявляться, но на продакшене с тяжелыми компонентами он вылезает как баг, который невозможно отловить без инструментов вроде React DevTools profiling. Вывод: Если вы не готовы к прерываниям рендера в Concurrent Mode — используйте useSyncExternalStore как единственный корректный способ синхронизации внешнего состояния с React.