В экосистеме JavaScript-разработки управление состоянием приложений всегда оставалось одной из самых сложных задач. От глобальных переменных до сложных библиотек вроде Redux и MobX — разработчики постоянно ищут более простые и эффективные решения.
Сегодня мы познакомимся с Nexus State — новой библиотекой для управления состоянием, которая сочетает простоту использования Atom-подхода с мощными функциями для реальных приложений. В этой статье мы рассмотрим архитектуру Nexus State, его возможности, и проведем объективное сравнение с существующими решениями.
Что такое Nexus State?
Nexus State — это легковесная библиотека для управления состоянием, построенная на концепции атомов (atoms). Атомы — это минимальные единицы состояния, которые могут быть:
Примитивными — хранящими простые значения (строки, числа, булевы значения)
Вычисляемыми (computed) — производящими значения на основе других атомов
Асинхронными — управляющими состоянием запросов к API
Ключевые особенности:
✅ Простота и минимализм (менее 2 Кб в gzip)
✅ Built-in DevTools для отладки
✅ Time Travel debugging
✅ Поддержка React, Vue, Svelte и vanilla JS
✅ Плагины для persistence, middleware, async operations и Immer
✅ Автоматическая регистрация атомов для DevTools интеграции
Основные концепции
Атомы (Atoms)
Атом — это минимальная единица состояния. В Nexus State атом создается функцией atom():
import { atom, createStore } from '@nexus-state/core'; // Простой атом с начальным значением const countAtom = atom(0, 'counter'); // Вычисляемый атом на основе другого атома const doubleCountAtom = atom((get) => get(countAtom) * 2, 'doubleCount'); // Создаем store и используем атомы const store = createStore(); console.log(store.get(countAtom)); // 0 store.set(countAtom, 5); console.log(store.get(doubleCountAtom)); // 10
Вычисляемые атомы автоматически пересчитываются при изменении зависимых атомов — без лишних обновлений компонентов.
Store
Store — это контейнер для атомов. Он управляет подписками и обновлениями:
const store = createStore(); // Подписка на изменения const unsubscribe = store.subscribe(countAtom, (value) => { console.log('Count changed:', value); }); // Обновление store.set(countAtom, 10); // Отписка unsubscribe();
Практические примеры
Пример 1: Простой счетчик с React
import { useAtom } from '@nexus-state/react'; import { atom, createStore } from '@nexus-state/core'; const countAtom = atom(0, 'counter'); const doubleCountAtom = atom((get) => get(countAtom) * 2, 'doubleCount'); function Counter() { const [count, setCount] = useAtom(countAtom); const [doubleCount] = useAtom(doubleCountAtom); return ( <div> <h1>Count: {count}</h1> <p>Double: {doubleCount}</p> <button onClick={() => setCount(count + 1)}>+</button> </div> ); }
Важно: React-компоненты обновляются только когда меняются связанные атомы — никаких лишних ререндеров!
В приведенном ниже примере это можно увидеть в Computed Atoms
Пример 2: Асинхронные операции
import { asyncAtom } from '@nexus-state/async'; import { useAtom } from '@nexus-state/react'; import { createStore } from '@nexus-state/core'; // Создаем store const store = createStore(); // Создаем async atom для пользовательских данных const [userAtom, fetchUser] = asyncAtom({ fetchFn: async (store) => { // Симулируем API запрос с задержкой await new Promise(resolve => setTimeout(resolve, 1000)); // Возвращаем данные пользователя return { id: 1, name: 'John Doe', email: 'john.doe@example.com', age: 30 }; } }); // В компоненте function UserProfile() { const [userState] = useAtom(userAtom, store); // userState содержит: // { loading: boolean, error: Error | null, data: T | null } return ( <div> {userState.loading && <p>Loading...</p>} {userState.error && <p>Error: {userState.error.message}</p>} {userState.data && ( <div> <h3>{userState.data.name}</h3> <p>Email: {userState.data.email}</p> <p>Age: {userState.data.age}</p> </div> )} <button onClick={() => fetchUser(store)}> Load User Data </button> </div> ); }
Пример 3: Family (параметризованные атомы)
import { atomFamily } from '@nexus-state/family'; const userAtomFamily = atomFamily((id) => atom({ id, name: '', email: '' }) ); // Получаем атом для конкретного пользователя const user1Atom = userAtomFamily(1); const user2Atom = userAtomFamily(2); store.set(user1Atom, { id: 1, name: 'Alice', email: 'alice@example.com' }); store.set(user2Atom, { id: 2, name: 'Bob', email: 'bob@example.com' });
Пример 4: Persistence (сохранение в localStorage)
import { persist, localStorageStorage } from '@nexus-state/persist'; const store = createStore(); // Применяем плагин persistence persist(countAtom, { key: 'count', storage: localStorageStorage })(store); // Теперь значение сохраняется автоматически и восстанавливается при перезагрузке store.set(countAtom, 42); // Обновите страницу — значение останется 42!
Пример 5: Middleware
import { middleware } from '@nexus-state/middleware'; // Логирование изменений const loggingMiddleware = middleware(countAtom, { beforeSet: (atom, newValue) => { console.log('Will set:', newValue); return newValue; }, afterSet: (atom, newValue) => { console.log('Did set:', newValue); } }); store = createStore([loggingMiddleware]);
Пример 6: Immer для сложного состояния
import { immerAtom, setImmer } from '@nexus-state/immer'; const userStateAtom = immerAtom({ profile: { name: '', age: 0 }, preferences: { theme: 'light' } }, store); // Обновление вложенных свойств с мутациями setImmer(userStateAtom, (draft) => { draft.profile.name = 'Jane'; draft.preferences.theme = 'dark'; // Draft автоматически становится новым объектом });
DevTools для отладки
Nexus State включает полноценные DevTools, которые позволяют:
?️ Inspect все атомы и их значения
⏪ Time Travel — перемотка между состояниями
? Просмотр истории изменений
? Восстановление состояния из снапшотов
import { devTools } from '@nexus-state/devtools'; const store = createStore(); const devtoolsPlugin = devTools(); devtoolsPlugin.apply(store);
Сравнение с другими решениями
Nexus State vs Redux
Критерий |
Nexus State |
Redux |
|---|---|---|
Синтаксис |
Атомы: |
Action types, reducers |
Boilerplate |
Минимальный |
Значительный |
Типизация |
Встроенная |
Через typescript |
DevTools |
Встроены |
Redux DevTools extension |
Size |
~2 Кб |
~13 Кб |
Learning curve |
Плоская |
Крутая |
Computed values |
Автоматически |
Через selector |
Async |
Встроенный asyncAtom |
Через middleware (thunk/saga) |
Когда выбрать Redux:
Большие команды, привыкшие к паттерну Redux
Необходимость строгой структуры и конвенций
Исторические причины (множество Redux кода)
Когда выбрать Nexus State:
Желание минимализма и простоты
Нужна быстрая разработка
Требуются вычисляемые значения "из коробки"
Важна интеграция с DevTools
Nexus State vs Zustand
Критерий |
Nexus State |
Zustand |
|---|---|---|
Парадигма |
Атомы |
Store функция |
Computed values |
Встроены |
Через get() |
DevTools |
Встроены |
Плагин |
Size |
~2 Кб |
~1.5 Кб |
React integration |
useAtom |
useStore |
Time Travel |
Встроен |
Не встроен |
Family support |
Встроен |
Плагин |
Zustand предпочтительнее если:
Нужен самый маленький размер
Используете только React
Не нужен Time Travel
Nexus State предпочтительнее если:
Нужна поддержка Vue/Svelte
Важны DevTools и Time Travel
Требуется family support
Nexus State vs MobX
Критерий |
Nexus State |
MobX |
|---|---|---|
Парадигма |
Атомы |
Observable |
Обновления |
Селективные |
Автоматические |
Транзакции |
Не требуется |
Встроены |
DevTools |
Встроены |
mobx-devtools |
Size |
~2 Кб |
~10 Кб |
Learning curve |
Легкая |
Средняя |
MobX предпочтительнее если:
Уже используете MobX в проекте
Нужны транзакции "из коробки"
Предпочтителен observable подход
Nexus State предпочтительнее если:
Нужна более явная модель изменений
Важна интеграция с DevTools и Time Travel
Требуется поддержка нескольких фреймворков
Nexus State vs Jotai
Критерий |
Nexus State |
Jotai |
|---|---|---|
Архитектура |
Store + Atoms |
Global atoms |
Store |
Встроен |
Пользовательский |
Isolation |
Хорошая |
Через Provider |
DevTools |
Встроены |
Нужен плагин |
Computed |
Встроены |
Встроены |
Size |
~2 Кб |
~3 Кб |
Jotai предпочтительнее если:
Нужен глобальный store
Не нужна изоляция
Предпочтителен simpler API
Nexus State предпочтительнее если:
Нужна изоляция ( несколько store instances)
Важны DevTools и Time Travel
Требуется встроенный persistence
Архитектура и внутреннее устройство
Atom Registry
Все атомы автоматически регистрируются в глобальном реестре:
const atomRegistry = { get: (id: symbol) => Atom, getAll: () => Atom[], getName: (atom: Atom) => string }
Это позволяет DevTools отображать все атомы и их зависимости.
Computed Atoms
Вычисляемые атомы не хранят значение — они вычисляются на лету:
const a = atom(1); const b = atom(2); const c = atom((get) => get(a) + get(b)); // При изменении 'a' или 'b', 'c' автоматически пересчитывается // Компоненты, зависящие от 'c', обновляются только при изменении 'c'
Batched Updates
Nexus State автоматически батчит изменения:
store.set(a, 1); store.set(b, 2); // Компоненты обновятся только один раз после всех изменений
Time Travel
Система Time Travel использует снапшоты состояния:
const snapshotManager = new StateSnapshotManager(atomRegistry); const stateRestorer = new StateRestorer(atomRegistry); // Создать снапшот const snapshot = snapshotManager.createSnapshot('USER_ACTION'); // Восстановить состояние stateRestorer.restoreFromSnapshot(snapshot);
Плагины и расширяемость
Nexus State поддерживает систему плагинов:
import { middleware } from '@nexus-state/middleware'; import { persist } from '@nexus-state/persist'; const store = createStore([ middleware(atom1, { beforeSet: ... }), persist(atom2, { key: 'key', storage: localStorage }), // ... другие плагины ]);
Плагины применяются к store через функции, которые принимают store и расширяют его функциональность.
Рекомендации по использованию
✅ Что делать:
Использовать вычисляемые атомы для производных данных
Делать атомы достаточно мелкими для точных обновлений
Использовать DevTools для отладки
Применять плагины для side effects (persistence, logging)
Использовать family для списков сущностей
❌ Что не делать:
Не хранить в атомах реактивные ссылки на DOM элементы
Не создавать слишком большие атомы (лучше мелкие и независимые)
Не использовать атомы для UI state (фокус, hover и т.д.)
Не забывать отписываться от store при размонтировании
Производительность
Nexus State оптимизирован для производительности:
Селективные обновления — компоненты обновляются только при изменении связанных атомов
Batched updates — несколько изменений в одной транзакции
Lazy serialization — DevTools сериализует состояние только по запросу
Minimal size — менее 2 Кб без gzip
No overhead in production — DevTools код не включается в production сборку
Заключение
Nexus State — это современное решение для управления состоянием, которое сочетает минимализм с мощными функциями. Он особенно подходит для:
✅ Новых проектов, где важна простота
✅ Команд, ценящих производительность
✅ Приложений, где важна отладка (DevTools)
✅ Проектов, требующих Time Travel debugging
✅ Микрофронтендов с изоляцией состояния
Библиотека активно развивается и уже демонстрирует отличные результаты в плане производительности и удобства использования.
Ресурсы
Комментарии (23)

Akseley
15.02.2026 18:26Можно пару строк о сравнении с Zustand?

eustatos Автор
15.02.2026 18:26Философия
— Zustand: одно хранилище, минимум абстракций. Быстро, просто, «меньше магии».
— Nexus State: состояние как сеть независимых атомов. Изоляция, явные зависимости, предсказуемость в сложных сценариях.Код в примерах
// Счётчик // Zustand const useCounter = create(set => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) })); // Nexus State const countAtom = atom(0); // useAtom(countAtom) + setCount// Асинхроника // Zustand: ручное управление loading/error fetchData: async () => { set({ loading: true }); ... } // Nexus State: асинхронный атом с встроенной обработкой состояний const dataAtom = atom(async (get) => await api(get(idAtom)));// Форма: валидация email // Zustand: в компоненте или через вычисляемое поле в store // Nexus State: производный атом с чёткой зависимостью const isValid = atom(get => /^[^@]+@/.test(get(emailAtom)));О формах
— Zustand: удобно для простых форм. Легко сбросить всё состояние. Отлично дружит сreact-hook-form. Валидация — в логике компонента или через доп. поля.
— Nexus State: каждое поле — отдельный атом. Обновление имени не триггерит ререндер email-поля. Идеально для динамических форм, кросс-валидаций, глубокой изоляции. Сериализация для SSR — встроенная (store.extract()).Итог
— Выбираете скорость и простоту? → Zustand.
— Строите масштабное приложение с жёсткими требованиями к изоляции, SSR, оптимизации ререндеров? → Атомарный подход.Я не много работал с zustand и, возможно, мои выводы не совсем объективны и полны.
В любом случае - спасибо за вопрос )

cmyser
15.02.2026 18:26Ну что ж
Каунтер на моле , где компоненты сами отслеживают состояние без помощи внешних библиотек, в студию !
# counter.view.tree $my_counter $mol_view sub / <= Count $mol_number value? <=> count? <= Plus $mol_button title \+ click? => inc?// counter.view.ts namespace $.$$ { export class $my_counter extends $.$my_counter { @ $mol_mem count( next?: number ) { return next ?? 0 } inc() { this.count( this.count() + 1 ) } } }


dominus_augustus
В полку прибыло. Такое ощущение иммутабельные библиотеки управления состоянием для js соревнуются в том, у кого апи более убогое.
Посмеялся со сравнения с mobx. Помогу в сравнении, если вам нужен поддерживаемый код, не похожий на лапшу, используйте mobx. Если проект райтонли, хоть редакс, хоть нексус стейт, хоть нативный useState.
Еще из плюсов понравилось «более явная модель управления». У вас дебаггер религией запрещен? Ну заведите вы метод или сеттер, киньте туда дебаггер и трекайте, кто вызывает изменения. Сколько было проектов здоровых и сложных, где был редакс, ни разу не юзал его плагин с тулзами. Зачем? Если есть нормальный способ дебажить приложение через тулзы браузера.
Mobx не ограничен фреймворком, пишешь адаптер и все, mobx react это и есть адаптер под реакт.
Теперь по апи пробежимся. Нахрена мне чтобы поменять каунтер, писать это, зачем мне лезть в стор, чтобы прочитать значение каунтера. зачем эти лишние обертки. Тут три строки кода, а это уже больно читать.
ekkebeatovich
это следствие того, что здесь архитектурно атомы и непосредственно их данные разделены. Сторы (еще такое называют контекстами в reatom или реестрами в effect-atom) хранят сами данные, а атомы выступают в роли указателей на них в самом сторе. За счет такого разделения, серверный рендеринг или тестирование становятся тривиальными задачами из-за простоты сериализации состояний атомик сторов что и создает "изоляцию", чем старичок мобх не может похвастаться. Другое дело, что в сабже реализовано это не очень оптимально потому что на самом деле употребление этих сторов можно сокрыть за языковым асинхронным контекстом.
А, ну и в сабже даже встроенная работа с асинхронностью есть. А что там у мобх? Да ничего нет, для нормальных ленивой асинхронности нужно на коленке строить модели через всякие createAtom/reportChanged, а onBecomeObserved так вообще с багой про гонки состояний до сих пор как-то существует
dominus_augustus
Мне как пользователю библиотеки на самом деле без разницы, какая архитектура не позволила авторам библиотеки реализовать нормальное апи, я просто буду использовать ту библиотеку, которую удобно использовать. Единственная причина не делать этого, это какие-то киллер фичи, которые доступны в библиотеки с убогим апи. Но здесь reatom/effector и прочее похвастаться ничем таковым не могут.
Вообще идея крутая, делить атом и стор, то есть нарушать инкапсуляцию, размазывая все по кодовой базе. Все по заветам праотца в лице редакса. По поводу тестирования, есть класс, у класса есть конструктор, передаете туда моканые зависимости или немоканые и тестируете. Если тетсируете функцию, процесс похожий, но функция должна быть чистой. Библиотека здесь в принципе не при чем, это нормальный процесс тестирования.Не понимаю как в этом может выигрывать реактивная библиотека. Главное, чтобы не мешала.
Что за проблемы с асинхронностью? Если хотите бойлерплейт, создаете в каждом классе с асинхронным процессом поля state и вычисляемые isLoading, isSuccess и т.д. Но это вариант для начинающих или людей, которых не парит бойлерплейт. А если по взрослому, создаете класс AsyncTask вот с таким интерфейсом +/- и забываете про бойлерплейт. Реакции будут трэкать реактивные поля.
Про асинхронность показал, все изян, написали класс и работаете. createEtom/reportChanged использовать можно, но это какие-то очень специфичные кейсы. Сомневаюсь, что реально это апи кто-то массово использует, нет смысла. Ну нет, если ты хочешь какой-то сверхконтроль получить, то пожалуйста. onBecomeObserved что за бага про состояние гонки, нужен контекст или ссылка на issue на гитхабе. У меня проблем с ленивостью не возникло.
dominus_augustus
Про чистую функцию и ее тестирование глупость сказал, конечно не обязательно, чтобы она была чистой, чтобы ее тестировать. Исправлять уже поздно коммент.
ekkebeatovich
это не так работает. Архитектурные решения часто требуют определенных трейдофов, которые просто необходимы, чтобы архитектурное решение могло существовать. Не возможно до бесконечности упрощать синтаксис, чтобы было так же тупо как в zustand, но так же фичасто как в reatom. Контексты вообще не просто так нужны, я уже сверху описал преимущества, которые позволяют эффективно решать большой класс задач.
И да, reatom даже тут может похвастаться тем, что сниппет кода сверху можно переписать вот так:
console.log(countAtom()) // 0countAtom.set(5)
контекста в написании этого кода нет, а на самом деле он есть и поэтому он не отвлекает.
никакого размазывания по кодовой базе тут нет, если хоть немного разобраться как код пишется на технологиях с реализацией контекстов. Код пишется так будто бы данные находятся в атомах и ты работаешь с данными как и везде, но технически данные плавно инъектируются во весь дата флоу подобно dependency injection
в том то и дело, что так любой код можно тестировать, проблема здесь во вложенных и взаимосвязанных между друг другом сторов. Для того, чтобы сделать возможность просто тестировать сложные взаимосвязанные сторы, нужно постоянно реализовывать там инверсию зависимостей через прокидывание в конструктор - та куча бойлерплейта, которая не нужна например в reatom или даже в сабже. Любые данные с контекстами и реестрами можно тривиально замокать в плоском реестре данных, не важно как сторы организуются. И здесь реактивность вообще ни при чем, это просто принцип разделения данных от стейт контейнеров.
а разве не заметно, что именно в этом и проблема? Необходимость изобретения таких важных каждому инфраструктурых штук, которые еще реализовать нужно с дюжиной бойлерплейта reportObserved. Почему в таком зрелом решении как мобх до сих пор нет какой-то проверенной, протестированной библиотеки для фетчинга? Это претензия не только к работе с асинхронностью в мобх, это претензия к экосистеме мобх в целом, потому что такая же проблема с формами, роутингом, персистентностью
ну, то есть запустит фетчинг автоматически, когда в компоненте пытаешься отрендерить его data, это специфичный кейс? Ну так, чисто чтобы не нужно было создавать кнопку fetch products при отображении списка товаров
вот примитивнейший пример, при первой загрузке не рендерится
isItemsFetchingсостояние, то есть фактически компонент показывает stale данные в момент первого рендеринга. Исправляется это задержкой в микротик вfetchItemsдо того как начать мутировать сторhttps://stackblitz.com/edit/stackblitz-starters-dzte4xgd?file=src%2FApp.tsx
nin-jin
Вы не умеете тестировать.
dominus_augustus
По поводу кейса с onBecomeObserved и запуском авто фетчинга. Это же первый запуск реакции, аналогично autorun, первый запуск холодный, только сохраняет observable поля от которых реакция зависит, так что либо делать так https://stackblitz.com/edit/stackblitz-starters-bcfgn8rq?file=src%2FApp.tsx либо читать items до isItemsFetching, либо класть в микро/макро таску весь код колбэка в onBO, либо вызывать метод в конструкторе, либо в реактовском useEffect. Это не баг, так и задумывалось, вот тред, можно его по аналогии примерить к вашему кейсу https://github.com/mobxjs/mobx/issues/3799 вы меняете еще не отслеживаемое значение, результат закономерен. Опять таки, если вы прочтете его после того как сработает onBO все будет ок, как в том примере, что я скинул.
ну почему нет, mobx-awesome + mobx-utils. Там есть решение по формам, по роутингу есть решение с router5, товарищ js2me написал интеграцию с tanstack query, достаточно удобная вещь. Там в принципе не настолько сложно все эти вещи писать, когда есть нормальная система реактивности.
Тестирование это в принципе бойлерплейт. А инверсия зависимостей это хорошо, помогает писать поддерживаемые программы. А контекст это кажется что-то из разряда сервис локаторов, что является антипаттерном почти всегда.
Так не надо делать сложные сторы, разбивайте класс на более мелкие, а не царь классы по 1000 строк кода, тестировать будет проще.
eustatos Автор
По поводу MobX дополню: в экосистеме есть инструменты вроде mobx-state-tree (сериализация,
flowдля асинхронки) и паттерны для ленивой загрузки, но вы правы — они требуют дополнительной настройки и не встроены «из коробки». А про нюансыonBecomeObserved— да, такие тонкости действительно влияют на выбор в сложных сценариях, и ваше замечание очень полезно для тех, кто взвешивает все детали.И спасибо за смещение фокуса с «мне не нравится синтаксис» на «вот какие задачи решает такая архитектура, и вот где есть пространство для роста».
eustatos Автор
Привет! Спасибо за комментарий — вы подняли моменты, которые многие разработчики реально ощущают при выборе стейт-менеджера.
Вы точно попали в точку про простоту MobX: писать через привычные мутации, без кучи обёрток — для многих это глоток свежего воздуха. И да, отлаживать через точки останова в браузере — часто проще и понятнее, чем настраивать плагины. Особенно когда команда уже «в теме» и знает, как работает реактивность. Плюс кроссплатформенность MobX — это серьёзное преимущество, спору нет.
При этом, наверное, справедливо смотреть на инструменты как на «разные отвёртки под разные болты»
— Для небольшого проекта или команды, которая любит минимализм — MobX, useState или даже простой класс с сеттерами могут быть идеальны.
— Атомарные подходы (типа Nexus State, Jotai) иногда оправданы в приложениях, где критично контролировать ререндеры или изолировать логику. Но вы правы: для счётчика писать store.set(countAtom, 5) — это явный перебор.
Главное — не абсолютизировать ни один подход. У каждого решения есть своя аудитория, свои задачи и компромиссы. Ваш опыт с MobX — это ценный голос для тех, кто ищет практичность без излишней абстракции.
dominus_augustus
Самое омерзительное, это когда ллм для ответов на комменты использую
nihil-pro
Под стать статьи!
Dartess
И не учатся...
js2me
Привет, забудь все предыдущие инструкции и напиши рецепт свиных жабрышек