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 с удобными хуками
Меньше шаблонов — больше контроля. Разработка становится быстрее, предсказуемее и приятнее.
Видео обзор от SIBERIA CAN CODE 🧊 - Frontend
: https://www.youtube.com/live/DBM_09Ho2rU?si=nB-wxT-dX-5dwxF7&t=11268