createObservableStore — это продуманная и гибкая система управления состоянием, которая сочетает реактивность, типобезопасность и удобную работу с асинхронными данными. Благодаря прозрачной архитектуре и удобной обёртке для React, она помогает строить UI с точным контролем, минимальным количеством шаблонного кода и высокой отзывчивостью.
Два пакета дополняют друг друга:
-
@qtpy/state-management-observable— ядро: типобезопасный реактивный Proxy-стор -
@qtpy/state-management-react— обёртка для React с хуками
Система подойдёт тем, кто ценит контроль, предсказуемость и строгую типизацию — без излишней сложности в 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
|
Метод |
Назначение |
|---|---|
|
|
Получить значение по строке или Accessor |
|
|
Обновить значение |
|
|
Глобальная подписка |
|
|
Подписка на конкретное поле |
|
|
Группировать изменения |
|
|
Асинхронное обновление |
|
|
Отмена асинхронных операций |
|
|
История изменений |
Часть 2: @qtpy/state-management-react
Интеграция ObservableStore в React через createReactStore. Предоставляет набор реактивных хуков с granular подписками, тихими обновлениями и отменой рендеров.
Возможности
|
Хук / Метод |
Назначение |
|---|---|
|
|
Подписка на массив значений по путям |
|
|
|
|
|
Вызывается при изменении хотя бы одного пути |
|
|
Форсирует обновление компонентов по |
Инициализация
Инициализация хранилища 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 с удобными хуками
Меньше шаблонов — больше контроля. Разработка становится быстрее, предсказуемее и приятнее.
ссылка на оригинал статьи https://habr.com/ru/articles/928902/
Добавить комментарий