惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
LINUX DO - 热门话题
S
Secure Thoughts
TaoSecurity Blog
TaoSecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
AI
AI
B
Blog RSS Feed
S
Schneier on Security
雷峰网
雷峰网
Schneier on Security
Schneier on Security
Help Net Security
Help Net Security
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
罗磊的独立博客
有赞技术团队
有赞技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
博客园 - Franky
Attack and Defense Labs
Attack and Defense Labs
The Cloudflare Blog
Webroot Blog
Webroot Blog
Last Week in AI
Last Week in AI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 叶小钗
美团技术团队
L
Lohrmann on Cybersecurity
T
The Blog of Author Tim Ferriss
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
Know Your Adversary
Know Your Adversary
O
OpenAI News
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cybersecurity and Infrastructure Security Agency CISA
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
I
InfoQ
GbyAI
GbyAI
T
Threatpost
C
Cisco Blogs

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Scoped Store: Когда useReducer не тянет, а Redux — слишком
IslamX · 2026-05-14 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели971

Туториал

Всем привет, я Ислам, фронтенд-инженер, сегодня хочу разобрать такую интересную связку для локальных сложных контекстов состояний в React проектах, а именно связку React Context+useState+useReducer и как мы его можем заменить на связку Context+Zustand+useRef получая заметный профит по следующим показателям:

- Масштабируемость

- Чистота

- Оптимизация

- Простота

Почти все разработчики работали со сложными локальными состояниями где глобального Redux/MobX было слишком много, а нативные решения на основе useState/useReducer+Context были слишком громоздкими и рано или поздно превращались во франкенштейна с задатками кривого Redux, отличаясь от проекта к проекту и даже от компонента к компоненту.

Разберем реальный кейс из продакшна

Контекст: Есть приложение в котором присутствует большой модуль - лента развлекательных видео (отрезки из мультфильмов, фильмов, блогов и тд) с образовательным уклоном в формате коротких видео (tiktok, reels, shorts) с интерактивными субтитрами.

Под капотом этой фичи стоит ИИ-модель которая на вход принимает видео в оригинальном языке и выдает объект с субтитрами и метаданными. Субтитры состоят из так называемых part. Part - это цельная составная единица, которая включает в себя смысловую часть текста (как минимум одно слово). Это сделано для того, чтобы не терять смысл при переводе но и не переводить весь текст целиком, позволяя юзеру изучать предложение по смысловым частям.

Модуль предназначен для изучения языка в духе Duolingo, но вовлечение достигается за счет коротких интерактивных видео. Проблема в том, что ИИ в некоторых случаях ошибается в таймингах, переводах и сегментации токенов в part. Чтобы исправлять такие ошибки, было принято решение сделать отдельный модуль в админке - редактор интерактивных субтитров.

Страница редактора состоит из:

- Видеоплеера (vidstack)

- Таймлайна с сегментами

- Теги (опционально)

- Глобальные действия (сохранить, удалить, добавить и тд)

Редактор должен уметь всё что нужно для полноценной правки субтитров: управление плеером, визуальный таймлайн с сегментами, полный CRUD над part-ами включая разбиение, слияние и миллисекундную точность таймингов, редактирование внутреннего содержимого каждого part, управление тегами и метку текущего времени на таймлайне.

Покажу кусочек интерфейса чтобы примерно представить о чем речь:

Часть интерфейса редактора субтитров

Часть интерфейса редактора субтитров

Суть проблемы: нам необходимо изолировать состояние в рамках страницы, но сделать так, чтобы это состояние мог получать и редактировать любой компонент из этой сложной композиции.

Эволюция проблемы: от наивности к «недоредаксу»

Первое что приходит в голову - использовать на топ-левеле useState/useReducer и шарить состояние и сеттер через context. Все довольно просто, шаблон понятный и очень популярный, каждый кто разрабатывал на react хоть раз прибегал к такой связке для избежания prop drilling и предсказуемого целостного управления сложным локальным стейтом.

В большинстве случаев разработка идет по пути прогрессивного проектирования чтобы не допустить оверинжиниринга, это в целом правильный подход но доставляет серьезные проблемы когда итоговая сложность конструкции недооценена.

Сначала рождается что-то типа этого:

const VideoEditorContext = createContext();

const VideoEditorProvider = ({children}) => {
  const [state, setState] = useState();

  return (
    <VideoEditorContext.Provider value={{ state, setState }}>
      {children}
    </VideoEditorContext.Provider>
  );
};

Но в какой-то момент стейт начинает раздуваться, и появляются жуткие конструкции:

setState(s => {
  ...s,
  key: {
    ...s.key,
    secondKey: {
      ...s.key.secondKey,
      finalKey: "some_value"
    }
  }
})

Тут мы понимаем что код стал достаточно громоздким и непонятным и настало время подключить useReducer, после этого наш код приобретает следующий вид:

const videoEditorReducer = (state, action) => {
  switch (action.type) {
    case "SELECT_PART":
      return ...;
    case "UPDATE_PART":
      return ...;
    case "SPLIT_PART": 
      return ...;
    case "SNAP_PARTS": 
      return ...;
    // ...ещё N кейсов
  }
};

И в провайдере:

const VideoEditorProvider = ({ children, initialData }) => {
  const [state, dispatch] = useReducer(videoEditorReducer, {
    parts: initialData.parts,
    selected: [],
    tags: initialData.tags,
  });

  return <VideoEditorContext.Provider value={{ state, dispatch }}>{children}</VideoEditorContext.Provider>;
};

Знакомо, да?

На этом этапе мы уже осознанно пишем свой недоредакс. Можно конечно же развить эту идею до функций action creator-ов чтобы нам не приходилось по строчному литералу понимать какое действие что означает - так мы окажемся еще ближе к провалу редаксу.

Вроде бы все работает, и уже лапши со спредами как в предыдущем варианте нет, проблему со сложным стейтом мы как-то решили, но, у нас есть есть еще одна серьезная проблема, особенно в контексте высокочастотных динамических компонентов - ад с ререндерами. На каждый чих мы получим ререндер всего и вся. Как ответственный инженер мы начинаем мемоизировать экшены и применить популярную технику разбиения контекста по назначению на две части:

- Контекст для изменения стейта

- Контекст для получения стейта

const VideoEditorStateContext = createContext();
const VideoEditorActionsContext = createContext();

const VideoEditorProvider = ({ children, initialData }) => {
  const [state, dispatch] = useReducer(videoEditorReducer, initialData);

  const actions = useMemo(() => ({
    selectPart: (id) => dispatch({ type: "SELECT_PART", payload: id }),
    updatePart: (id, data) => dispatch({ type: "UPDATE_PART", payload: { id, data } }),
    splitPart: (id, splitTime) => dispatch({ type: "SPLIT_PART", payload: { id, splitTime } }),
    // ...
  }), []); // actions не меняются, dispatch стабилен

  return (
    <VideoEditorActionsContext.Provider value={actions}>
      <VideoEditorStateContext.Provider value={state}>
        {children}
      </VideoEditorStateContext.Provider>
    </VideoEditorActionsContext.Provider>
  );
};

Выглядит умно. Но проблема до конца не решена, потому что стейт то у нас по-прежнему единый, и компонент который слушает state.tags будет ререндериться когда изменится state.parts . Мы идем еще дальше и дробим сам стейт на отдельные сущности:

const PartsContext = createContext();
const TagsContext = createContext();
const SelectionContext = createContext();
<VideoEditorActionsContext.Provider>
  <PartsContext.Provider>
    <TagsContext.Provider>
      <SelectionContext.Provider>
        {children}
      </SelectionContext.Provider>
    </TagsContext.Provider>
  </PartsContext.Provider>
</VideoEditorActionsContext.Provider>

В целом уже неплохо. Много кода, красивый и популярный паттерн - мы молодцы… Но что если в структуру стейта нужно будет добавить еще одну сущность, например - state.originalVideoLanguage ? Еще один контекст? Очевидно, мы движемся не туда. Архитектура диктует нам дробить логически единое состояние на куски ради оптимизации. Контекстов становится больше, провайдеры выстраиваются в елочку, а новый разработчик открывает файл и …

...закрывает.

Мы боролись с prop drilling, а получили provider drilling.

Context + Zustand + useRef

И тут выходит на сцену та самая связка Context+Zustand+useRef. Zustand - одна из самых удачных имплементаций стейт-менеджмента вокруг нативного реактовского useSyncExternalStore на основе pub/sub паттерна. Используя useRef мы можем применить фабричный подход к созданию изолированного стора и хранить результат в рефе - это гарантирует нам стабильность ссылки к стору. С помощью Провайдера контекста мы выставляем искусственное ограничение по применению этого стора. Это конечно не запрещает использовать фабрику в других местах - решается на уровне соглашений в команде что любой Zustand стор который создается через фабричную функцию не должен использоваться напрямую - только через соответствующий провайдер (Scoped Store).

Что я имею в виду под фабрикой (обратите внимание на нормализацию данных, это необходимо для того чтобы селектор был стабильным и доступ был O(1)):

export const createShortStore = (initData: ShortDetails) => {
  const { parts, partIds, lineOrders } = normalizeParts(initData.parts);
  return createStore<Store>()(
    immer(() => ({
      short: initData,
      parts,
      tags: initData.tags
        ? initData.tags.map((tag) => ({ value: tag.replace(/^source:/, ""), isSource: tag.startsWith("source:") }))
        : [],
      partIds,
      lineOrders,
      editPartData: null,
      selected: [],
      currentSelected: null,
    })),
  );
};

Создаем контекст:

type StoreType = ReturnType<typeof createShortStore>;

export const ShortStoreContext = createContext<StoreType | null>(null);

Создаем хук для получения стора по селектору на основе useContext , данный подход хорош тем что оптимизация происходит на уровне useSyncExternalStore а не с помощью useMemo :

export function useShortStore<T>(selector: (state: Store) => T): T {
  const store = useContext(ShortStoreContext);

  if (!store) {
    throw new Error("...");
  }

  return useStore(store, selector);
}

Кроме этого можем создать еще один хук - для получения инстанса стора:

export function useShortStoreApi() {
  const store = useContext(ShortStoreContext);
  return store;
}

И наконец наш провайдер:

export const StoreProvider: FC<Props> = ({ children, data }) => {
  const storeRef = useRef(createShortStore(data));
  return <ShortStoreContext.Provider value={storeRef.current}>{children}</ShortStoreContext.Provider>;
};

И хук для экшнов, изолируем наши функции для изменения стора:

export function useShortActions() {
  const store = useShortStoreApi();

  function addPart() {
		store.setState(...)
  }

  function mergeParts() {
		store.setState(...)
  }
	
	function splitParts() {
		store.setState(...)
  }
	
	function updateTag() {
		store.setState(...)
  }
	
	function changeTokensOrder() {
		store.setState(...)
  }
  
  // и еще какие-то экшены
  
  return {
    addPart,
    mergeParts,
    splitParts,
    updateTag,
    changeTokensOrder,
    //...
  };
}

Доступ к стору получаем только в моменте изменения, никакой прямой подписки на стор. Можем пойти еще дальше раздробив наш хук для экшенов по сущностям - но это уже по необходимости.

Все)

parts, tags, selected - всё это теперь живёт в одном сторе, без елочки провайдеров и без useMemo как костыля. Компонент PartsList подписывается только на partIds и не знает что происходит с тегами и с part-ми. Компонент Part подписывается к своему сегменту по id. Панель редактирования читает только выбранный part — и не реагирует на смену currentTime. Метка времени живет отдельно, напрямую слушая currentTime через сигналы Vidstack. Каждый компонент платит ровно за то что потребляет.

Добавить новую сущность - одна строка в сторе. Экшны не завязаны к стору. Никаких новых провайдеров, новых контекстов и елочек.

Итог

- Масштабируемость - стор растёт линейно, не экспоненциально

- Чистота - один провайдер, один стор, понятные экшены в виде функций

- Оптимизация - селекторы вместо useMemo, useSyncExternalStore под капотом

- Простота - новый разработчик открывает файл и не закрывает его)

Context+useState/useReducer не плохой инструмент. Он хорошо справляется с простыми локальными состояниями, но когда сложность растёт - начинает разваливаться.

Zustand+useRef+Context - это не оверинжиниринг и не попытка притащить глобальный стейт менеджер куда не просят. Это тот же привычный паттерн с провайдером и хуком, только с нормальным инструментом внутри.