Эта статья - логическое продолжение моих экспериментов с реактивностью в JavaScript.
В современной фронтенд-разработке реактивность очень важна. Но в React она может быть достигнута посредством библиотек (MobX, к примеру). Поэтому я задался целью создать легковесный реактивный движок достаточно простой для понимания и тестирования.
Вот что я хочу вам показать 👉 https://www.npmjs.com/package/@pravosleva/reactive-engine
🇷🇺 Более подробную инструкцию на русском можно найти здесь. В исходниках можно найти тесты, описывающие возможности движка, и более продвинутые примеры отслеживания реактивных состояний (помимо тех что будут приведены в этой статье).
🇬🇧 Инструкция на английском также доступна.
NPM-пакет @pravosleva/reactive-engine решает именно эту задачу, предоставляя разработчикам специализированный движок (синглтон-экземпляр ReactiveEngine) для управления реактивными состояниями и автоматического вычисления зависимостей на чистом JavaScript.
Основная идея инструмента
Архитектура @pravosleva/reactive-engine базируется на концепции направленного графа вычислений. Вместо того чтобы вручную вызывать функции обновления при каждом изменении переменной, вы описываете связи между данными один раз. Движок сам берет на себя задачу по цепочке обновить все зависимые узлы.
Ключевые преимущества такого подхода:
- Разделение ответственности (Decoupling). Бизнес-логика полностью отделена от слоя представления (UI-рендеринга).
- Минимизация лишних вычислений. Движок кэширует промежуточные результаты и пересчитывает только то, что действительно изменилось.
- Предсказуемый Data Flow. Потоки данных движутся строго в одном направлении, исключая бесконечные циклы обновлений.

Как это устроено под капотом?
Подобные реактивные движки обычно реализуют три фундаментальных типа сущностей:
- Атомы / Сигналы (Atoms / Signals / Observables) - базовые ячейки хранения данных. Они хранят примитивы или объекты и генерируют событие всякий раз, когда их значение перезаписывается.
- Вычисляемые свойства (Computed / Computed Nodes) - узлы графа, которые зависят от других ячеек. Они лениво (lazy) вычисляют свое значение и автоматически подписываются на те атомы, которые были прочитаны в процессе выполнения их функции.
- Эффекты / Реакции (Effects / Reactions) - конечные точки графа, которые не возвращают новых данных, но выполняют побочные эффекты (Side Effects): сохраняют данные в LocalStorage, отправляют API-запросы или дергают методы обновления интерфейса.
Пример использования (Чистый JS)
Архитектура @pravosleva/reactive-engine базируется на концепции направленного графа вычислений. Вместо того чтобы вручную вызывать функции обновления при каждом изменении переменной, вы описываете связи между данными один раз через свойство .value. Движок сам берет на себя задачу по цепочке обновить все зависимые узлы.
Давайте представим стандартную задачу: расчет стоимости корзины товаров с учетом динамической скидки и отправкой аналитики. С использованием реактивного движка на чистом JS код принимает декларативный вид:
import { ReactiveEngine } from '@pravosleva/reactive-engine'
// Инициализируем движок (Синглтон)
const engine = new ReactiveEngine()
// Инициализируем базовые состояния (Сигналы)
const price = engine.signal(1000)
const quantity = engine.signal(2)
const discountPercent = engine.signal(10) // 10%
// Описываем вычисляемые узлы графа (Computed)
// Движок автоматически отслеживает, к каким .value было обращение внутри функций
const baseTotalPrice = engine.computed(() => price.value * quantity.value)
const finalPrice = engine.computed(() => {
const total = baseTotalPrice.value
const discount = (total * discountPercent.value) / 100
return total - discount
})
// Создаем побочный эффект (Effect)
engine.effect(() => {
console.log(`[UI Update] Итоговая сумма к оплате: ${finalPrice.value} руб.`)
})
// Работа с реактивным графом
// В консоли сразу сработает эффект при инициализации:
// "[UI Update] Итоговая сумма к оплате: 1800 руб."
// Меняем количество товара (Операция ++ вызывает сеттер свойства .value)
quantity.value++
// Движок автоматически пересчитает baseTotalPrice -> finalPrice -> вызовет эффект:
// В консоли: "[UI Update] Итоговая сумма к оплате: 2700 руб."Пример использования в React
В библиотеке @pravosleva/reactive-engine реактивные сущности (сигналы) предоставляют доступ к своему состоянию через свойство .value. Благодаря встроенным геттерам и сеттерам, изменение значения через конструкцию .value++ автоматически перехватывается движком и запускает цепочку обновлений.
Создание хранилища
// ~/store.ts
import { ReactiveEngine } from '@pravosleva/reactive-engine'
const engine = new ReactiveEngine()
// Базовая ячейка состояния (Сигнал)
export const counterSignal = engine.signal(0)
// Вычисляемый узел графа (Computed). Автоматически зависит от counterSignal.
export const doubleComputed = engine.computed(() => counterSignal.value * 2)
// Побочный эффект (Effect). Срабатывает при каждом изменении counterSignal.
engine.effect(() => {
console.log(`[Лог] Текущий счетчик изменился: ${counterSignal.value}`)
})Интеграция в React-компонент
Для связки с UI-слоем React библиотека предоставляет удобный хук useReactiveValue, который подписывает компонент на изменения (но можно и без него в стиле MobX через observer - примеры есть в доке).
// Counter.tsx
export const Counter = () => {
// Хук автоматически подпишется на изменения и вызовет ререндер
const count = useReactiveValue(counterSignal)
const doubleCount = useReactiveValue(doubleComputed)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<h3>Счетчик: {count}</h3>
<p>Удвоенное значение (Computed): {doubleCount}</p>
{/* Операция ++ разворачивается в counterSignal.value = counterSignal.value + 1 */}
{/* Сеттер свойства .value перехватывает это действие и обновляет граф */}
<button onClick={() => counterSignal.value++}>Увеличить</button>
<button onClick={() => counterSignal.value--}>Уменьшить</button>
</div>
)
}Еще пример из доки: Асинхронные ресурсы с зависимостями от нескольких сигналов
Если ваш сетевой запрос зависит от фильтров, пагинации или ID пользователя, объедините их в computed, чтобы resource автоматически перезапускал fetch-логику и отменял потерявшие актуальность запросы:
// apiStore.ts
import { engine } from './store'
export const userIdSignal = engine.signal(1, 'userId')
export const tabSignal = engine.signal<'posts' | 'todos'>('posts', 'tab')
// Объединяем сигналы в единый вычисляемый массив зависимостей
const requestDeps = engine.computed(() => {
return [userIdSignal.value, tabSignal.value] as const
});
// Создаем реактивный асинхронный ресурс
export const userDataResource = engine.resource(
async ([userId, tab], abortSignal) => {
const res = await fetch(`https://typicode.com{userId}/${tab}`, {
signal: abortSignal, // Передаем нативный токен отмены
});
if (!res.ok) throw new Error('Ошибка при загрузке данных')
return res.json()
},
requestDeps, // Передаем зависимости
'userData'
)В компоненте это выглядит максимально декларативно:
// UserProfile.tsx
import React from 'react'
import { useReactiveValue } from '@pravosleva/reactive-engine'
import { userIdSignal, tabSignal, userDataResource } from './apiStore'
export const UserProfile = () => {
// Читаем объект состояния ресурса: { data, loading, error }
const { data, loading, error } = useReactiveValue(userDataResource);
const tab = useReactiveValue(tabSignal);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button onClick={() => { tabSignal.value = 'posts'; }}>Вкладка Посты</button>
<button onClick={() => { tabSignal.value = 'todos'; }}>Вкладка Задачи</button>
<button onClick={() => { userIdSignal.value += 1; }}>Следующий пользователь</button>
<h4>Текущая вкладка: {tab}</h4>
{loading && <p>Загрузка данных по сети...</p>}
{error && <p style={{ color: 'red' }}>Произошла ошибка: {error.message}</p>}
{data && <pre>{JSON.stringify(data.slice(0, 3), null, 2)}</pre>}
</div>
)
}Пакет @pravosleva/reactive-engine - это отличный выбор для разработчиков, которым нужен полный контроль над реактивностью без необходимости тащить за собой тяжеловесные экосистемы вроде MobX или RxJS. Он позволяет структурировать хаотичные потоки данных в понятный, легко тестируемый и высокопроизводительный вычислительный граф.