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

Комментарии (9)


  1. isumix
    18.07.2025 10:12

    У Фьюзора нет своего стейт менеджера, но можно интегрировать любую библиотеку примерно такой функцией:

    import { update } from "@fusorjs/dom"; // updates component
    const connectComponentToDom = (self) => anyObservable.subscribe(() => update(self)); // => unsubscribe()
    
    // usage:
    const x = <SomeComponent mount={connectComponentToDom} />

    Как вашу библиотеку можно было бы интегрировать во Фьюзор?


    1. TheOnlyFastCoder2 Автор
      18.07.2025 10:12

      .


  1. nihil-pro
    18.07.2025 10:12

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

    Разрешите не согласится.

    const [name, setName] = userStore.useField("user.name");
    const [age, setAge] = userStore.useField("user.age");
    
    userStore.useEffect(["user.age"], ([age]) => {
      userStore.$.user.name = "qtpy"; 
    });
    const userStore = createReactStore(initialState, [], {
      customLimitsHistory: ($) => [
        ["user.age", 5],
        [(t) => $.items[t(1)], 3],
      ],
    });
    useStore([(t) => $.board[t(row)][t(col)]])
    const store = createObservableStore<AppState, DepthPath>(initialState, [], {
      customLimitsHistory: ($) => [
        ["user.age", 5],
        [(t) => $.items[t(1)], 3],
      ],
    });

    createObservableStore — это реактивная система нового поколения

    Увы, но нет.


    1. TheOnlyFastCoder2 Автор
      18.07.2025 10:12


      Я показывал несколько вариантов и специально сделал так чтобы у разработчиков был выбор чтобы каждый мог использовать ту парадигму обновления состояния, которая ближе к их архитектурным предпочтениям или привычкам команды — будь то прямые мутации через proxy, более декларативные обновления через строковые пути с поддержкой typescript, или гибкие селекторы, обеспечивающие точечный контроль.

      Вы можете свойствами на прямую взаимодействовать через proxy store.$.user.age = 5 или через систему подписок при помощи строковых путей store.update('user.age', 5) или же при помощи селекторов как в простонародье называется sotre.update(() => store.$.user.age, 4) или store.update((t) => store.$.items[t(index)] , 2)


      1. nihil-pro
        18.07.2025 10:12

        store.$.user.age = 5

        Напрямую это так: store.user.age = 5

        store.update('user.age', 5)

        Лишний вызов функции + непривычный способ обращения к свойству + высокая вероятность опечататься.

        sotre.update(() => store.$.user.age, 4)

        Лишний вызов уже двух функций + лишний чужеродный $.

        Меньше шаблонов, разработка становится быстрее, предсказуемее и приятнее тогда, когда вы не заставляете разработчика использовать сомнительные конструкциии/обертки для того, что бы удовлетворить потребности вашей «реактивной системы нового поколения».


        1. TheOnlyFastCoder2 Автор
          18.07.2025 10:12

          На счет  store.user.age = 5 - разработчики сами должны решать, работать ли напрямую с proxy-объектом store.$ или использовать систему подписок.

          Я ни раз писал о типобезопасных строках в этой статье , в оф. документации все расписано @qtpy/state-management-observable  .
          если вы опечатаетесь store.update('user.agesdfsdfs', 5) вам выдаст ошибку


        1. TheOnlyFastCoder2 Автор
          18.07.2025 10:12

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

          
          //до:
          store.get((t) => store.$.board[t(row)][t(col)])
          store.get(() => store.$.user.age)
          
          //после:
          store.get(($, t) => $.board[t(row)][t(col)])
          store.get(($) => $.user.age) 

          и в правду так лучше стало , а то "store" может иметь любое название , и тогда из-за длинного название плохая будет читабельность.


  1. winkyBrain
    18.07.2025 10:12

    это реактивная система нового поколения

    какие громкие слова) это видимо от незнания? ведь уже 3 года существует обёртка над сигналами от команды Preact, которая даёт возможность при изменении состояния сигнала перерисовать значение конкретного тега в разметке(всех тегов, которые ссылаются на сигнал), не тревожа ничего вокруг никакими ререндерами. и не понадобятся никакие createReactStore и прочий бойлерплейт


    1. TheOnlyFastCoder2 Автор
      18.07.2025 10:12

      Да я с вами полностью солидарен однозначно что сигналы это очень хороший инструмент и удобный , и на счет громких слов. Но не все его могут использовать или интегрировать без последствий. Некоторые разработчики — включая команду React — считают, что сигналы нарушают базовые принципы реактивной архитектуры, особенно при конкурентном рендеринге. Есть опасения, что они ломают однонаправленный поток данных, напоминают старые решения вроде KnockoutJS и могут усложнить масштабирование крупных приложений. Поэтому, несмотря на свою эффективность, сигналы подходят далеко не всем — особенно в проектах с высокими требованиями к стабильности и предсказуемости.