Как стать автором
Поиск
Написать публикацию
Обновить

CreateObservableStore: реактивный store с гранулярными подписками и идеальной интеграцией с React

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.3K

createObservableStore — это продуманная и гибкая система управления состоянием, которая сочетает реактивность, типобезопасность и удобную работу с асинхронными данными. Благодаря прозрачной архитектуре и удобной обёртке для React, она помогает строить UI с точным контролем, минимальным количеством шаблонного кода и высокой отзывчивостью.

Два пакета дополняют друг друга:

Система подойдёт тем, кто ценит контроль, предсказуемость и строгую типизацию — без излишней сложности в API.

Безопасность и масштабируемость

Система createObservableStore построена с учётом крупных проектов, где важно избегать ошибок при росте кода. Благодаря типобезопасным путям через DepthPath, разработчики получают:

  • Предотвращение "магических" строк: пути описываются функциями или типизированными ключами, что исключает случайные ошибки

  • Глубокое автодополнение в IDE: при указании путей доступна контекстная подсказка с учётом вложенности объекта

  • Снижение риска при рефакторинге: изменение структуры состояния сразу вызывает ошибку компиляции, исключая "тихие" баги

Эти механизмы делают архитектуру устойчивой и прогнозируемой — особенно при разделении стора на модули и работе в команде.

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

userStore.useEffect(["user.age"], ([age]) => {
  console.log("Возраст обновился:", age);
  userStore.$.user.name = "qtpy"; // → компонент, подписанный на 'user.name', будет отрисован
});

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

Часть 1: @qtpy/state-management-observable

Типобезопасный реактивный стор, не зависящий от фреймворка. В её основе лежит обёртка Proxy, которая реализует прозрачную реактивность через store.$, позволяя перехватывать любые чтения и записи, включая косвенные мутации массивов через такие методы, как push, splice, sort и другие. Вся система построена вокруг системы подписок, основанной на Map: подписки привязываются к конкретным путям (строковым или Accessor-функциям), а уведомления рассылаются только при реальных изменениях, что определяется через сравнение snapshot-хэшей до и после обновления. Такой механизм позволяет избежать лишних перерендеров и сохраняет высокую производительность.

import { createObservableStore } from "@qtpy/state-management-observable";

interface User {
  name: string;
  age: number;
}

interface AppState {
  user: User;
  items: number[];
  theme: string;
}
const initialState: AppState = {
  user: { name: "Alice", age: 30 },
  items: [1, 2, 3],
  theme: "light",
};
type DepthPath = 2;
const store = createObservableStore<AppState, DepthPath>(initialState, [], {
  customLimitsHistory:  [
    ["user.age", 5],
    [($, t) => $.items[t(1)], 3],
  ],
});

// Точечная подписка
store.subscribeToPath("user.age", (newAge) => {
  console.log("Возраст изменился:", newAge);
});

// Тихое обновление
store.update("user.age", 35, { keepQuiet: true });

// История
store.undo("user.age");
store.redo("user.age");

Работа с массивами

В createObservableStore работа с массивами устроена так, что при каждом изменении создаётся snapshot до и после мутации. Сравнение хешей этих snapshot’ов определяет, было ли реальное изменение. Если данные действительно изменились — система отправляет точечное уведомление подписанным компонентам.

  • Перед мутацией создаётся snapshot

  • После мутации — новый snapshot

  • Сравнение хешей определяет — было ли изменение

Примеры:

store.$.items.push(2323); // → вызов подписки
store.$.items[2] = 42; // → точечное уведомление

store.update("items", (prev) => {
  prev.push(99);
  return prev;
});

store.update("items", (prev) => prev); // → изменений нет → уведомлений нет

Полный API

Метод

Назначение

get(path)

Получить значение по строке или Accessor

update(path, value, opts?)

Обновить значение

subscribe(callback, keys?)

Глобальная подписка

subscribeToPath(path, cb)

Подписка на конкретное поле

batch(callback)

Группировать изменения

asyncUpdate(...)

Асинхронное обновление

cancelAsyncUpdates(path?)

Отмена асинхронных операций

undo(path) / redo(path)

История изменений

Часть 2: @qtpy/state-management-react

Интеграция ObservableStore в React через createReactStore. Предоставляет набор реактивных хуков с granular подписками, тихими обновлениями и отменой рендеров.

Возможности

Хук / Метод

Назначение

useStore(paths)

Подписка на массив значений по путям

useField(path)

[value, setValue] с setValue.quiet()

useEffect(paths, fn)

Вызывается при изменении хотя бы одного пути

reloadComponents(paths)

Форсирует обновление компонентов по cacheKeys

Инициализация

Инициализация хранилища createReactStore в React-приложении осуществляется точно так же, как и при работе с createObservableStore. Обе функции принимают одинаковые параметры, включая начальное состояние, depth-параметры и настройки истории. Это обеспечивает единообразие подхода и лёгкую миграцию между обёртками, позволяя разработчикам использовать реактивную модель без потери типобезопасности или производительности.

import { createReactStore } from "@qtpy/state-management-react";

const userStore = createReactStore(initialState, [], {
  customLimitsHistory: [
    ["user.age", 5],
    [($, t) => $.items[t(1)], 3],
  ],
});

Компонентный пример: UserCard

import { userStore } from "./store";

export const UserCard = () => {
  const [name, setName] = userStore.useField("user.name");
  const [age, setAge] = userStore.useField("user.age");

  // Реакция на изменение
  userStore.useEffect(["user.age"], ([age]) => {
    console.log("Возраст обновился:", age);
    userStore.$.user.name = "qtpy";
  });

  return (
    <div>
      <h2>{name}</h2>
      <p>Возраст: {age}</p>
      <button onClick={() => setAge((cur) => cur + 1)}>+</button>
      <button onClick={() => userStore.undo("user.age")}>Undo</button>
      <button onClick={() => userStore.redo("user.age")}>Redo</button>
      <button onClick={() => userStore.reloadComponents(["user.age"])}>
        reload
      </button>
    </div>
  );
};

Тихие обновления для оптимизации рендера

В некоторых случаях обновление данных не требует вызова перерисовки компонента — особенно если значение изменяется вне контекста отображения или используется только в логике. Для этого предусмотрен механизм "тихих обновлений" через setTheme.quiet(). Такой подход помогает снизить нагрузку на UI и повысить производительность, сохраняя при этом реактивность и контроль над состоянием.

const [theme, setTheme] = store.useField("theme");
setTheme.quiet("dark"); // Тихо изменили тему

Практический кейс: Игра «15-пятнашек»

Эта реализация на createReactStore показывает, как granular-подписки и реактивные обновления могут использоваться не только в бизнес-логике, но и в интерактивных интерфейсах. В игре каждая плитка (Tile) подписывается только на конкретный элемент массива board[row][col], а компонент реагирует лишь при изменении соответствующего поля. Все действия — сдвиги, проверки победы, счётчик ходов — выполнены через batch, update и undo, что делает логику прозрачной и производительной.

  • Подписка на вложенное значение: useStore([($, t) => $.board[t(row)][t(col)]])

  • Реакция на флаг решения: useField(($) => $.isSolved)

  • Работа с batch() для группировки обновлений

  • Проверка победы через checkSolved() и реактивное обновление isSolved

Код и компоненты разбиты на модули (store.ts, Tile.tsx, PuzzleGame.tsx) — удобно для масштабирования или адаптации под другие игры.

-> Ознакомиться с примером в документации

Вывод

createObservableStore — это реактивная система с новой концепцией:

  • гранулярность - подписки на уровень конкретного свойства

  • Контроль истории изменений и отмены

  • Полная типобезопасность в TypeScript

  • Чистая интеграция в React с удобными хуками

Меньше шаблонов — больше контроля. Разработка становится быстрее, предсказуемее и приятнее.

Видео обзор от SIBERIA CAN CODE 🧊 - Frontend
: https://www.youtube.com/live/DBM_09Ho2rU?si=nB-wxT-dX-5dwxF7&t=11268

Теги:
Хабы:
+2
Комментарии9

Публикации

Ближайшие события