open atlas
↑ К треку
Паттерны React RXP · 07 · 01

Когда тянуться к стору

Внешний стор оправдан, только когда подъём состояния и контекст перестают масштабироваться — дальние потребители, частые обновления, штормящие контекст, или сохранность между маршрутами. Иначе это переусложнение или дублированное серверное состояние.

RXP Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Ты умеешь делить состояние в React: размести его рядом с потребителем, подними к ближайшему общему родителю и потянись к контексту, когда цепочка пропсов становится длинной. Эта лестница покрывает подавляющее большинство состояния — и именно здесь большинство приложений должно остановиться. Потом кто-то добавляет Zustand, Jotai или Redux Toolkit, и вдруг всё состояние живёт там, потому что «глобально проще», и теперь у тебя два источника правды и загадка с перерисовками, которую надо отлаживать.

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

Цель

После этого урока ты можешь назвать четыре давления, оправдывающих внешний стор, — состояние, общее для дальних ветвей, частые обновления, которые заштормили бы контекст, сохранность между сменами маршрута и сильные потребности в devtools/отладке; написать минимальный стор Zustand с подписками на основе селекторов; и распознать два режима отказа — глобальный стор для состояния, которое делят два соседа (переусложнение), и копирование серверного состояния в стор (дублированное серверное состояние).

1

Лестница по умолчанию: колокация → подъём → контекст. Стор — это ступень, на которую ты поднимаешься, только когда они перестают масштабироваться. Большинство порывов «нам нужен стор» на самом деле — «это состояние на два уровня ниже, чем надо» или «этот пропс пробрасывается на четыре уровня». Подъём и контекст чинят это бесплатно, без новой зависимости и без второго источника правды. Senior-приём — сперва исчерпать лестницу.

// ступень 1: колокация — состояние живёт рядом со своим единственным потребителем
function SearchBox() {
  const [q, setQ] = useState("");
  return <input value={q} onChange={(e) => setQ(e.target.value)} />;
}

// ступень 2: подъём — ближайший общий родитель владеет общим состоянием
// ступень 3: контекст — прекрати проброс редко меняющегося общего состояния (тема, авторизованный пользователь)

Стор — это ступень 4. Ты тянешься к нему, когда ступени 1–3 дают либо проброс пропсов на глубину, которую не отрефакторить, либо контекст, чьи обновления перерисовывают слишком многое. Не раньше.

2

Давление первое: дальние ветви. Когда два далеко разнесённых поддерева делят состояние, подъём заталкивает его абсурдно высоко, а контекст становится широкой сетью. Если палитре команд в шапке и панели деталей в глубоком маршруте обоим нужен один и тот же выбор, ближайший общий родитель — это корень приложения, так что подъём туда означает протягивание пропсов или провайдера через всё, что между ними. Стор позволяет каждому потребителю подписаться напрямую, без общего предка, занимающегося прокладкой.

// стор, потребляемый двумя несвязанными ветвями — общего родителя между ними нет
const useSelection = create<{ id: string | null; select: (id: string) => void }>(
  (set) => ({ id: null, select: (id) => set({ id }) })
);

function HeaderPalette() {
  const select = useSelection((s) => s.select); // подписан на то, что не меняется
  return <button onClick={() => select("user-42")}>Jump to user</button>;
}
function DeepDetailPanel() {
  const id = useSelection((s) => s.id); // перерисовывается, только когда меняется id
  return <p>Selected: {id ?? "none"}</p>;
}

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

3

Давление второе: частые обновления, которые заштормили бы контекст. У контекста нет селектора — каждый потребитель провайдера перерисовывается на любое изменение значения, даже потребитель, читающий только несвязанное поле. Для состояния, которое обновляется много раз в секунду (позиция живого курсора, жест перетаскивания, потоковый счётчик, canvas), один контекст превращает одно обновление в шторм перерисовок по всему дереву. Стор подписывается по селектору, так что компонент перерисовывается, только когда меняется его срез.

// высокая частота: движения указателя срабатывают десятки раз в секунду
const useCursor = create<{ x: number; y: number; move: (x: number, y: number) => void }>(
  (set) => ({ x: 0, y: 0, move: (x, y) => set({ x, y }) })
);

function Crosshair() {
  // только этот компонент перерисовывается при move — соседи, читающие другие срезы, нет
  const { x, y } = useCursor((s) => ({ x: s.x, y: s.y }));
  return <line style={{ transform: `translate(${x}px, ${y}px)` }} />;
}

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

4

Давления третье и четвёртое: сохранность между маршрутами и devtools. Стор живёт вне дерева компонентов, поэтому переживает размонтирование — и предоставляет единый инспектируемый объект состояния. Поднятое состояние и состояние контекста умирают, когда их владеющее поддерево размонтируется при смене маршрута; сохранить его — значит поднять выше роутера, что часто означает… стор. Стор также даёт одно место, чтобы инспектировать, путешествовать во времени и логировать каждый переход — история Redux DevTools / middleware Zustand, — что является настоящей ценностью, когда переходы состояния достаточно сложны, чтобы «что изменилось и почему» было реальной ценой отладки.

import { persist } from "zustand/middleware";

// переживает смены маршрута и перезагрузки; один инспектируемый объект
const useDraft = create(
  persist<{ text: string; setText: (t: string) => void }>(
    (set) => ({ text: "", setText: (text) => set({ text }) }),
    { name: "draft" } // сохраняется в хранилище, восстанавливается между маршрутами/перезагрузками
  )
);

Эти четыре давления — дальность, частота, сохранность, отлаживаемость — единственные причины, побеждающие более простую лестницу. Если ни одно не применимо, у тебя нет задачи формы «стор».

Разбор примера

Переключатель темы и меню пользователя, которым нужна одна и та же theme, — стор-первый против лестница-первая. Два соседних компонента в панели настроек делят одно редко меняющееся значение.

Рефлекс «стор-первый» переусложняет это:

// переусложнено: глобальный стор для состояния, которое делят два соседа
const useTheme = create<{ theme: "light" | "dark"; toggle: () => void }>((set) => ({
  theme: "light",
  toggle: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
}));

function SettingsBar() {
  return (<div><ThemeToggle /><UserMenu /></div>); // оба лезут в глобальный стор
}

Работает, но теперь глобальный синглтон уровня приложения владеет состоянием, которое делят два соседних компонента, — лишняя косвенность, зависимость и состояние, достижимое отовсюду, откуда не следует. Подъём строго проще:

// под размер: подними к ближайшему общему родителю, прокинь вниз
function SettingsBar() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
  return (
    <div>
      <ThemeToggle theme={theme} onToggle={toggle} />
      <UserMenu theme={theme} />
    </div>
  );
}

Теперь поменяй давления: если бы theme читали десятки дальних компонентов (ступень 3, контекст — оно редко меняется и широко общее) или если оно должно переживать смены маршрута и перезагрузки (ступень 4, сохраняемый стор), ответ меняется. Стор оправдывает своё место только когда давление реально. Здесь два соседа делят одно значение, ближайший общий родитель прямо тут — побеждает подъём, а тяга к стору — это режим отказа «переусложнение» в пальто «глобально проще».

Частая ошибка

Второй режим отказа тоньше и куда более распространён: копирование серверного состояния в стор. Команды делают GET /users, кладут массив в Zustand, и теперь стор держит устаревший снимок, который надо вручную инвалидировать, перезапрашивать и сверять, — плохо переизобретая кэширование, дедупликацию и ревалидацию. Серверное состояние — не клиентское: им владеет сервер, оно может измениться за твоей спиной и нуждается в кэшировании с политикой свежести. Это территория TanStack Query / SWR / RSC, а не стора. Держи в сторе по-настоящему клиентское состояние — выбор в UI, черновики, переключатели, эфемерное взаимодействие — и пусть библиотека серверного кэша владеет всем, что пришло по сети. Смешивание этих двух — это то, как стор становится вторым, лгущим источником правды.

Граничные случаи

«Частый контекст — это нормально, если я разделю провайдеры и применю мемоизацию» — отчасти верно, и стоит знать, где проходит граница. Ты можешь приручить контекст, разделив состояние на много провайдеров и обернув каждого потребителя в memo, и для горстки полей это разумно. Но по мере роста числа независимо обновляющихся полей дерево разделения провайдеров становится собственным налогом на сопровождение, и ты вручную собрал худшую версию того, что стор на основе селекторов даёт бесплатно. Стор — не магия: под капотом это useSyncExternalStore с покомпонентными селекторами. Суждение в том, когда стоимость ручного разделения контекста превышает стоимость одной маленькой зависимости-стора; частое состояние со множеством полей пересекает эту границу быстро.

Проверь себя
Викторина

Твоя команда делает GET /projects и хочет, чтобы список был доступен на нескольких экранах, поэтому коллега хранит массив в Zustand и вручную перезапрашивает его при монтировании. По этому уроку, стор — правильный дом для этого состояния?

Итог

Лестница состояния — это колокация → подъём → контекст, и она обрабатывает большую часть состояния без новой зависимости. Внешний стор (Zustand, Jotai, Redux Toolkit) — четвёртая ступень, оправданная ровно четырьмя давлениями: состояние, общее для дальних ветвей без вменяемого общего родителя, частые обновления, которые заштормили бы контекст без селектора, сохранность между сменами маршрута и перезагрузками и сильные потребности в devtools/отладке, когда переходы сложны. Минимальный стор Zustand даёт подписки по селектору, так что каждый компонент перерисовывается только на своём срезе — это и делает дальность и частоту доступными по цене. Но у стора есть два режима отказа, отмечающих его как неверный инструмент: тяга к глобальному синглтону, когда два соседа могли бы просто поднять состояние к общему родителю (переусложнение), и копирование серверного состояния в стор вместо того, чтобы дать библиотеке серверного кэша владеть свежестью (дублированное серверное состояние). Стор оправдывает своё место только когда подъём и контекст явно перестали масштабироваться — подбирай его под давление, а не под порыв «глобально проще».

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.