Привет, меня зовут Артём Арутюнян и я автор менеджера состояния Reatom. Этим постом открывается серия обучающих материалов на русском языке, документация на английском доступна на официальном сайте.

А оно вам надо? Думаю, да, потому что Reatom — это универсальное решение, которое позволяет легко пошарить глобальное состояние за микроскопическую (2.5KB) цену, эффективно строить самодостаточные и переиспользуемые логические модули гигантских приложений или просто сделать ваш сетевой кеш реактивным с помощью дополнительного пакета @reatom/async.

В этой статье мы кратко пройдёмся по мотивации и истории, а потом разберём основные фичи и примеры их использования вместе с биндингами к React.js. Похожий разбор есть в виде скринкаста.

▍ Мотивация


Я ленивый идеалист с каким-то пунктиком на перформанс (у меня часто были маломощные ноутбуки и смартфоны) и я всё время попадал на проекты с сотнями тысяч или миллионом строк кода, хотя иногда касался и мелких стартапов. И всегда мне хотелось использовать одно решение, не переключаясь между контекстами, которое будет и маленькое, и эффективное, и позволяло бы использовать энтерпрайзнутые паттерны, которые нужны, когда цена любого рефакторинга может быть огромной.

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

Сложно сделать хорошо всё и сразу, поэтому эволюция Reatom заняла годы.

▍ История


Первый релиз был осенью 2019-го, хотя ему предшествовали почти два года исследований. Началось всё в феврале 2018-го, тогда меня передёрнуло от function-tree, и я решил сделать с подобным апи убийцу редакса (тогда это было популярным занятием). Далее история долгая: погружение в дзен вывода типов TypeScript, десятки прототипов, постоянные попытки выжать лучшее из современных технологий. Исследование алгоритмов обхода графов для решения проблемы глитчей. Рост комьюнити и попытки писать понятную документацию. Обслуживание инфраструктуры монорепы. Погружение в теорию баз данных, которые я администрировал ещё в 2014-м, но не задавался вопросами подкапотной архитектуры. Переосмысление архитектуры веб-приложений и состояния как явления. Один из артефактов всего этого — недавняя статья «Что такое состояние», в которой изложены ключевые принципы архитектуры менеджера состояния.

Но главное — первая LTS и вторая версия реатома пытались быть совместимы с редаксом, и сколько я ни старался, нормально это сделать не выходило, он просто фундаментально сломан:

  • O(n) сложность, где n — количество подписчиков;
  • единая очередь для подписчиков и вычисляемых значений, из-за чего в селекторах нет атомарности;
  • невозможность батчинга (диспатча нескольких экшенов).

Бойлерплейт для меня всегда был меньшей проблемой, но вы просто посмотрите на эту разницу между тулкитом(!) и реатомом. По ссылке используется пакет @reatom/framework, который включает в себя базовый набор самых часто используемых пакетов и просто реэкспортит из них всё для удобства установки и импорта. В последние пару лет требования к развитой экосистеме всё важнее. В 2020-м было нормально иметь маленькую библиотеку и дать на откуп пользователей писать и публиковать в NPM свои хелперы. Но сейчас индустрия уже повзрослела и предъявляет взвешанные требования к экосистеме, её слаженности и поддержке. Все пакеты Reatom хранятся в монорепе, что позволяет тестировать любое изменение со всеми зависимостями и синхронизировать релизный цикл, сделав его предсказуемым.

Это что касается технического аспекта поддержки, в общем же политика выглядит так: нечётные релизы считаются LTS и поддерживаются несколько лет. Первая версия поддерживалась три года, сейчас можно подменить импорты и использовать код на ней дальше с новыми фичами и дальнейшей поддержкой. Текущая третья версия (@reatom/core@3.x.x) будет актуальна ещё несколько лет. Раз в год возможны небольшие ломающие изменения от рефакторинга типов.

▍ Базовые сущности


Cперва взглянем на код базового примера из этой песочницы:

import { action, atom } from '@reatom/core'
import { useAction, useAtom } from '@reatom/npm-react'

const inputAtom = atom('')
const greetingAtom = atom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`)
const onChange = action((ctx, event) =>
  inputAtom(ctx, event.currentTarget.value),
)

export const Greeting = () => {
  const [input] = useAtom(inputAtom)
  const [greeting] = useAtom(greetingAtom)
  const handleChange = useAction(onChange)

  return (
    <>
      <input value={input} onChange={handleChange} />
      {greeting}
    </>
  )
}

Это самый базовый пример трёх ключевых сущностей: контекст, атом, экшен. На их основе можно реализовать большинство популярных паттернов из ФП или ООП и расширять по необходимости дополнительными фичами, больше десятка существующих пакетов этому пример. Но давайте разберём каждую строчку детальней.

Конечно, больше всего вопросов вызывает ctx. Это некий глобальный DI-контейнер на стероидах, заточенный под стейт-менеджемент. Он позволяет читать актуальный стейт атома ctx.get(anAtom), подписываться на него ctx.subscribe(anAtom, newState => sideEffect(newState)) и планировать сайд-эффекты во время транзакции, но об этом попозже. Главное, что нужно запомнить — ctx прокидывается первым аргументом в большинстве колбэков реатома и каждый раз приходит новый (под капотом содержит весь стек предыдущих контекстов вплоть до глобального).

inputAtom — базовый атом и кирпичик, с которого всё начинается. Это переменная для реактивного доступа и обновления данных. Хорошей практикой является разделять все ваши состояния на множество атомов с примитивными значениями. Для пользователей редакса это может быть чуждо, но в процессе использования всё больше будет ощущаться профит от такого подхода.

Базовый атом может быть вызван как функция с новым значением или редьюсером этого значения: countAtom(ctx, 1) и countAtom(ctx, state => state + 1). Такой вызов функции возвращает новое значение атома. В типах такой атом называется AtomMut (mutable atom).

Благодаря такому апи код быстро и удобно парсить глазами: ctx.get и ctx.spy получают значение атома, а doSome(ctx) и someAtom(ctx, value) мутируют.

greetingAtom — вычисляемый атом, который вызывает переданную функцию при первой подписке и с помощью метода spy в контексте подписывается на переданный атом и получает его значение. Это map и combine в одном флаконе, только гибче и удобнее. В типах такой атом называется просто Atom. У вычисляемых значений реатома есть две киллер-фичи, каждая из которых отдельно встречается в ФП (редакс) или ООП (мобыкс) мире, но я ещё не встречал их вместе.

Первое — если вычисление в редьюсере (да, вторым аргументом приходит предыдущий стейт) упадёт с ошибкой, все предыдущие изменения в текущей транзакции откатятся и будет соблюдена атомарность (рассказывал об этом здесь). Это важный аспект, позволяющий не допустить неконсистентные данные, и он важен для крупных приложений. Базовое поведение React практически такое же, даже жёстче, всё приложение размонтируется (в случае отсутствия componentDidCatch).

Второе — вы можете использовать spy в любом порядке (привет, правила хуков реакта). Вы можете применять его в условии и подписываться только на нужные атомы, когда это действительно актуально, что оптимизирует автоматически ваши потоки данных и помогает избавится от лишних вычислений.

const listAtom = atom([])
const listAggregatedAtom = atom(ctx => aggregate(ctx.spy(listAtom)))
const listViewAtom = atom((ctx) => {
  if (ctx.spy(isAdminAtom)) {
    return ctx.spy(listAggregatedAtom)
  } else {
    return ctx.spy(listAtom)
  }
})

Подобный код на реселекте или ФРП-библиотеке, скорее всего, получал бы вместе isAdmin, list и listAggregated и способствовал избыточным вычислениям (для isAdmin === false). Конечно, в теории можно описать селекторы, которые будут делать примерно то же самое, но на практике так не заморачиваются и получают очередную каплю в замедление приложения. В реатоме такие условные подписки — базовый принцип.

Удобно использовать в вычисляемом атоме и простой ctx.get для чтения какого-то значения — это не создаёт подписку, но гарантированно отдаёт самый актуальный стейт переданного атома.

На самом деле атомы не хранят значения, а являются лишь объектом с метаданными и ключом WeakMap-контекста, где и хранятся все стейты и связи между атомами. Это позволяет прозрачно и безопасно инстанцировать цепочки вычислений и упрощает SSR и тестирование.

onChange — экшен, хелпер для батчинга изменений. Если у вас есть несколько атомов для последовательного обновления, каждое изменение будет тригерить их зависимые вычисления и подписчиков. Что бы забатчить вычисления, можно использовать колбэк в ctx.get(() => {...}) или просто создать выделенный экшен и произвести все апдейты в нём. Экшены удобны тем что им можно, как и атомам, давать имена (второй аргумент), что в дальнейшем упрощает дебаг @reatom/lgger.

Про TypeScript, реатом разрабатывается с большим фокусом на автоматическом выводе типов и всегда старается понять переданные данные. Если вам необходимо затипизировать параметры экшена, просто укажите их тип у них же: action((ctx, event React.ChangeEvent) => ...). Больше рекомендаций по описанию типов ищите в документации.

Под капотом экшен — это атом со временным стейтом, хранящий params вызова и возвращённый payload. С ним можно делать всё то же, что и с атомом: подписываться через ctx.subscribe для сайд-эффектов и ctx.spy в вычисляемом атоме. Например, можно в вычисляемом атоме получить данные другого атома только при срабатывании какого-то экшена — это редкий, но очень удобный способ оптимизации. Больше примеров и возможных паттернов разберём в следующих статьях.

Стоит упомянуть о ctx.schedule, который позволяет планировать сайд-эффекты, как useEffect в реакте. Его можно вызывать где угодно, но чаще всего это пригождается в экшенах.


const onSubmit = action((ctx) => {
  const input = ctx.get(inputAtom)
  inputAtom(ctx, '')
  ctx.schedule(() => api.submit({ input }))
})

Переданный в ctx.schedule колбэк будет вызван после всех чистых вычислений, но до вызова подписчиков — это удобно, т. к. иногда эффекты просто сохраняют что-то в localStorage или делают другие не чистые, но синхронные операции и вызывают ещё апдейты. Подробности есть в документации, в общем же реатом старается всегда максимально отложить вызов подписчиков, чтобы избежать лишних ререндеров и предоставить самый последний и актуальный стейт. У редакса с этим ситуация радикально хуже.

▍ @reatom/npm-react


Все пакеты-адаптеры имеют префикс платформы (npm, web, node), подробнее об этом можно почитать в документации.

Думаю, использование useAtom и useAction понятно и практически не нуждается в комментариях :) Хотя несколько вещей всё же нужно учесть.

В документации к npm-react описаны обязательные инструкции по подключению реатома в провайдер реакта и настройке батчинга для старой (<18) версии реакта.


import { createCtx } from '@reatom/core'
import { reatomContext } from '@reatom/npm-react'

const ctx = createCtx()

export const App = () => (
  <reatomContext.Provider value={ctx}>
    <Main />
  </reatomContext.Provider>
)

Почему useAtom возвращает кортеж, как useState? Потому что его можно использовать как useState! Вторым значением приходит колбэк обновления, который принимает новое значение или редьюсер.

export const Greeting = () => {
  const [input, setInput] = useAtom(inputAtom)
  const [greeting] = useAtom(greetingAtom)

  return (
    <>
      <input value={input} onChange={e => setInput(e.currentTarget.value)} />
      {greeting}
    </>
  )
}

Конечно, это будет работать только для AtomMut — невычисляемого атома с примитивным начальным значением.

Но не будем на этом останавливаться. Вы можете не создавать отдельный атом и использовать его в разных местах через импорты, а можете использовать примитивное значение в useAtom, и атом под капотом будет создан автоматически, а ссылку на него можно получить в третьем элементе кортежа. Также вы можете передать и вычисляемый редьюсер в useAtom, как и в обычный atom, и описать какой-то селектор прямо в компоненте.


export const Greeting = () => {
  const [input, setInput, inputAtom] = useAtom('')
  const [greeting] = useAtom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`, [inputAtom])

  return (
    <>
      <input value={input} onChange={e => setInput(e.currentTarget.value)} />
      {greeting}
    </>
  )
}

Это может быть удобно для некоторых оптимизаций, смысл которых в передаче получившегося атома по ссылке в пропсы нижележащим компонентам. Подробнее этот паттерн мы разберём в следующих статьях.

▍ Заключение


Это всё, чем хотелось поделиться в первой статье, но реатом таит в себе ещё множество фич и архитектурных практик, которые сильно помогают в написании более чистого и тестируемого кода. Но обо всём поэтапно, ждите серию постов.

Можно лишь отметить, что любителям ФРП стоит обратить внимание на пакет @reatom/lens, а сторонникам более классической архитектуры — взглянуть на пакет @reatom/hooks, который позволяет писать более изолированный код, приближенный к акторам.

Ах да, и про реактивный кеш. Пакет @reatom/async в связке с базовыми фичами реатома даёт большую часть фич react-query, а какие-то даже превосходит, всего за 3.4KB (gzip).

Смотрите больше примеров на соответствующей странице документации, добавляйтесь в Телеграм-канал и чат. И, конечно, оставляйте ваши комментарии и вопросы ниже.

Играй в нашу новую игру прямо в Telegram!

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


  1. markelov69
    03.01.2023 12:23
    -2

    Getters / Setters - не, не слышал. Буду делать так, чтобы пользоваться было максимально неудобно.

    До MobX'a конечно ещё очень и очень далеко.


    1. artalar Автор
      03.01.2023 12:38
      +7

      Геттеры и сеттеры вносят избыточную семантику и сложность, они менее явные и я всегда стараюсь их избегать. Дебаг мутабельных данных намного сложнее, а в реатоме из коробки асинхронный трекинг контекста-стека, чего мобыксу скорее всего никогда не получить. Про бандлсайз и так ясно, из-за этого мобыкс очень далек от универсального решения, но и перф у него до двух раз хуже... Очень специфическое решение, этот ваш мобыкс. И экосистема у него не большая и не развивается.


      1. markelov69
        03.01.2023 12:58
        +7

        Геттеры и сеттеры вносят избыточную семантику и сложность

        С чего это вдруг? Неправда.

        async doSome() {
          this.fetching = true;
          this.doSomeCallsCount++; 
          
          try {
            const response = await apiReq('GET /alala');
            this.items = response.items;
            this.error = null;
          } catch (e) {
            this.error = e.message;
          } finally {
            this.fetching = false;
          }
        }

        Где тут избыточность и сложность? Геттеры и сеттеры под капотом, они не используются при написании кода.

        они менее явные и я всегда стараюсь их избегать

        Взгляните на пример с кодом выше, в каком месте вы видите хоть грамм не явности?

        Дебаг мутабельных данных намного сложнее

        Да ладно? А может вы в блокноте код пишете?)

        Webstorm:
        Клик правой кнопкой -> Find Usages


        VS Code:
        Клик правой кнопкой -> Find All References

        Итого: за 5 секунд нашли все места где читается и где изменяется нужная нам переменная, если сразу не понятна причина бага, то добавляем console.log в нужные места и вуаля, мы элементарно нашли причину бага.

         Про бандлсайз и так ясно, из-за этого мобыкс очень далек от универсального решения

        А да? Т.е. React + React-DOM размером в более чем 150kb это норм, а MobX добавляющий ещё пару копеек, это уже проблема? Не смешите. Или вы всё ещё в 2010 году живете?

        но и перф у него до двух раз хуже

        О, это великая проблема если в цикле из миллиона итерацией он окажется на сколько-то ms медленнее. Это же прям относится к дело и в реальной жизни мы такие проекты и пишем.

        Очень специфическое решение, этот ваш мобыкс

        Вообще кошмар :D

        И экосистема у него не большая и не развивается

        Зачем экосистема полностью готовому, самодостаточному, законченному и работающему проекту? Просто бери его и вперед.


        1. Pijng
          03.01.2023 14:02
          +9

          Есть такая профессия – MobX любить.


        1. artalar Автор
          03.01.2023 14:15
          +3

          Так, ну мне теперь очевидно что вы видите исключительно то что хотите видеть, но по аргументам вашим пробегусь, для интереса других читателей.

          С чего это вдруг? Неправда.

          Нельзя добавить логики и не уменьшить семантику, это база в теории ЯП и инженерии. Мобыкс предполагает скрытое добавление семантики с которой нужно жить и учитывать: присвоение проперти в переменной не делает ее реактивной, геттеры в форыче могут заметно повлиять на перф, и в общем где-то можно не намеренно подписываться на то что не нужно, например ридонли id для `key` в рендеринге списка элементов.

          Это не катастрофическая проблема, просто, по моему мнению, избыточная.

          Где тут избыточность и сложность?

          Совсем печально что вы не знаете ответ на этот вопрос. Мобыкс, в зависимости от настроек, может не забатчить несколько последовательных апдейтов после авейта.

          пример с кодом выше

          Зачем мне этот пример? В реальном приложении все сложнее. Кстати, в мобыксе нет сущности event / action и нельзя построить событийную модель, а иногда надо. Например, как вы будете отправлять лог аналитике при клике по кнопке? Засунете этот код прям в хендлер? Грязно, грязно!

          Далее. Как вы связали дебаг и инспектирование кода я не совсем понял, но уточню что речь об инспектировании результата асинхронных потоков: в мобыксе с этим сложнее, в реатоме просто работает из коробки даже на проде 9и ничего не стоит, сами внутренние структуры данных так задизайнены).

          150kb это норм

          Ну вам норм, но не все на реакте пишут. Реатом предоставляет адаптер для свелта, в разработке адаптер для преакта и солида. Кто туда и зачем возьмет мобыкс - не ясно, а вот реатом сможет пригодиться.

          Зачем экосистема

          Ну тот же @reatom/async очень удобен, в мобыксе такого не хватает. А еще у нас почти готовы пакеты для офлайн синхронизации и форм, позже будет пакет для роутинга. Намного лучше когда это все разрабатывается одной группой лиц.


          1. markelov69
            03.01.2023 14:48

            Нельзя добавить логики и не уменьшить семантику, это база в теории ЯП и инженерии. Мобыкс предполагает скрытое добавление семантики с которой нужно жить и учитывать: присвоение проперти в переменной не делает ее реактивной, геттеры в форыче могут заметно повлиять на перф, и в общем где-то можно не намеренно подписываться на то что не нужно, например ридонли id для `key` в рендеринге списка элементов.

            Это не катастрофическая проблема, просто, по моему мнению, избыточная.

            Бла бла бла, много воды и всё не по делу. Только 1 "аргумент", который легко проверить.

            Вот он:

            геттеры в форыче могут заметно повлиять на перф

            Проверяем:

            class Test {
                count = 1;
            
                get c() {
                    return this.count;
                }
            }
            
            const test = new Test();
            
            let result = 0;
            console.time('t');
            for (let i = 0; i < 100000; i++) {
                result += test.c;
            }
            console.timeEnd('t');
            console.log(result);
            О ужас, 1ms в сто тысяч итерацией и по мимо get тут 2 раза идет инкремент в каждой итерации
            О ужас, 1ms в сто тысяч итерацией и по мимо get тут 2 раза идет инкремент в каждой итерации

            Вывод: Ваш "аргумент" это просто пустые слова не подтвержденные практикой и реальными проектами и задачами.

            Совсем печально что вы не знаете ответ на этот вопрос. Мобыкс, в зависимости от настроек, может не забатчить несколько последовательных апдейтов после авейта.

            Да-да, конечно же я ничего не знаю и вообще первый день в разработке.

            import { configure } from 'mobx';
            
            let reactionSchedulerTimeout = null;
            
            // Configure MobX to auto batch all sync mutations without using action/runInAction
            configure({
                enforceActions: 'never',
                reactionScheduler: (f) => {
                    clearTimeout(reactionSchedulerTimeout);
                    reactionSchedulerTimeout = setTimeout(f);
                },
            });

            Ой, а вот и автобатчинг.

            Зачем мне этот пример? В реальном приложении все сложнее. Кстати, в мобыксе нет сущности event / action и нельзя построить событийную модель, а иногда надо. Например, как вы будете отправлять лог аналитике при клике по кнопке? Засунете этот код прям в хендлер? Грязно, грязно!

            Да, засуну прямо в хэндлер т.к. это самое очевидное что может быть и в любой момент времени, любой разработчик будет понимать что происходит на каждой строчки кода.

            А грязно это вот этот ваш лютый говнокод:

            export const Greeting = () => {
              const [input, setInput, inputAtom] = useAtom('')
              const [greeting] = useAtom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`, [inputAtom])
            
              return (
                <>
                  <input value={input} onChange={e => setInput(e.currentTarget.value)} />
                  {greeting}
                </>
              )
            }

            Далее. Как вы связали дебаг и инспектирование кода я не совсем понял, но уточню что речь об инспектировании результата асинхронных потоков: в мобыксе с этим сложнее, в реатоме просто работает из коробки даже на проде 9и ничего не стоит, сами внутренние структуры данных так задизайнены).

            Бла бла бла, опять много слов и ничего по делу. Речь идёт о мухах в недрах снежной массе вещества в коробке передач.

            Ну вам норм, но не все на реакте пишут

            Так пусть пишут, мне то что с того.

            Ну тот же @reatom/async очень удобен

            Вообще нет, это просто очередная лапша. А все async хэлперы реализуются элементарно с помощью MobX.

            И вообще взрослые дядьки пишут плагины и трансформеры, чтобы оставлять исходный код красивым и чистым, а на этапе "компиляции" уже его модифицировать как нужно, например заворачивать компоненты в observer'ы, например разруливать race condition'ы, debounce'ы и т.д и т.п. и при этом чтобы не приходилось писать убогие конструкции и лапшу. А просто бизнес логику сверху вниз, слева на право.


            1. DonVietnam
              04.01.2023 10:34
              +3

              Вы хоть представляете как в вашем супер тесте поведет себя движок JS? Рискну предположить, что запускали вы его в V8, что означает, что цикл ваш он скорее всего на одном месте повертел и удалил в целях оптимизации, нельзя так мерить производительность в JS.


              1. nin-jin
                04.01.2023 10:52

                Зачем вы спорите с человеком, который не первый день в разработке? Очевидно же, что мобыкс не делает в геттере ничего кроме чтения одного поля.


              1. markelov69
                04.01.2023 13:27

                Вот вам прямо с MobX'ом

                const { makeAutoObservable } = require('mobx');
                
                class Test {
                    count = 1;
                
                    constructor() {
                        makeAutoObservable(this);
                    }
                }
                
                const test = new Test();
                
                let result = 0;
                console.time('t');
                for (let i = 0; i < 100000; i++) {
                    result += test.count;
                }
                console.timeEnd('t');
                console.log(result);
                Кошмар, 6ms вместо 1ms на 100тыс итераций
                Кошмар, 6ms вместо 1ms на 100тыс итераций



          1. Alexandroppolus
            03.01.2023 21:59

            Например, как вы будете отправлять лог аналитике при клике по кнопке? Засунете этот код прям в хендлер? Грязно, грязно!

            Как это бестпрактисно выглядит на Реатоме?

            Ну тот же reatom/async очень удобен, в мобыксе такого не хватает. 

            Имеется довольно неплохой mobx-utils, там есть несколько асинхронных паттернов. Ну а что-то совсем кастомное запилить тоже нетрудно. Мобх - это удочка, можно взять и наловить любую рыбу.


            1. artalar Автор
              04.01.2023 05:00

              Архитектурный смысл реактивности в неявном связывании для лучшей изоляции каждого модуля. Аналитика - совсем отдельная задача и было бы хорошо что бы она подключалась со стороны и лениво, не трогая код фичи и не влезая в логику. Реактивное программирование именно про это - мы берем паблик интерфейсы с сигнатурой subscribe и добавляем где-то вне реакции.

              Те модуль аналитики просто импортирует нужные атомы / экшены и подписывается на них. В данном примере это будет так

              ```javascript
              // anal.ts
              import { onUpdate } from '@reatom/hooks'
              import { onSomeClick } from '~/features/some/model'

              onUpdate(onSomeClick, () => log('onSomeClick'))
              ```

              Я так делал, вполне удобно, код фичи становится заметно чище.

              Это наивное решение. Есть еще возможность собирать логи автоматически - подписаться вообще на все логи через `ctx.subscribe((logs, error) => logs.forEach((patch) => patch.proto.name && log(patch.proto.name, patch.state)))`, но тут нужно побольше фильтров наставить каких-то, конечно.


              1. nin-jin
                04.01.2023 10:11

                • При реализации фичи придётся править ещё и код аналитики в совсем другом месте.

                • Аналитика потянет в бандл все фичи, даже те, что не используются.


                1. artalar Автор
                  04.01.2023 10:57
                  +1

                  Ну давайте теперь вообще код не изолировать и писать огромный монолит? Очевидно же что код разбивается на внутреннюю реализацию и внешние интерфейсы и последние реже меняются и свободнее переиспользуются в других модулях, тестах, аналитике.

                  Зачем аналитике тянуть в бандл лишние фичи тоже не ясно, есть очевидные способы разделения кода на чанки.


                  1. nin-jin
                    04.01.2023 11:11

                    Какие-то странные крайности. Ну вот аналитика получается огромным монолитом, которая знает о всех 100500 экшенах белее 9000 модулей.

                    Потому, что она их все импортирует к себе.


              1. Alexandroppolus
                04.01.2023 12:06

                Есть еще возможность собирать логи автоматически - подписаться вообще на все логи через ctx.subscribe((logs, error) =&gt; logs.forEach((patch) =&gt; patch.proto.name &amp;&amp; log(patch.proto.name, patch.state))),

                https://mobx.js.org/analyzing-reactivity.html#spy

                Там же, кстати, и для дебага тулзы


                1. artalar Автор
                  04.01.2023 12:25

                  ну вот проблема в том что в мобыксе все равно нет эвент-лайк сущности и сам клик по кнопке в мобыксе никак не отметится. Реатом, с моей точки зрения, в этом плане лучше архитектуру навязывает.


                  1. nin-jin
                    04.01.2023 12:32

                    В интерфейсе может быть несколько кнопок, которые выполняют одно и то же действие. В аналитике обычно хотят знать по какой именно кнопке был клик, а не только какое действие было выполнено где-то в недрах приложения.


                    1. artalar Автор
                      04.01.2023 13:00

                      Эм, а у одних и тех же кнопок будет один и тот же хендлер?? Если логику строить на определении атрибутов в event.target - да, но таким редко кто страдает (и зачем?).

                      В любом случае, в модуле аналитике лог будет выглядеть так `onUpdate(onClick, (ctx, { params: [event] }) => log('click', event.target.dataSome))`


                  1. Alexandroppolus
                    04.01.2023 14:23

                    Отслеживать пользовательские действия - это, вообще говоря, не задача для стейт-манагерской либы.

                    Отслеживание кликов я бы сделал старым добрым делегированием, с дата-атрибутом. Просто, декларативно, навешивается сбоку. Единственный изъян - stopPropagation будет мешаться, ну да он давно признан антипаттерном и практически не юзается.


        1. nin-jin
          04.01.2023 12:23
          +1

          Где тут избыточность и сложность?

          Сравните с этим:

          @mem doSome() {
            this.doSomeCallsCount++
            return apiReq('GET /alala').items
          }


          1. e_Hector
            04.01.2023 18:53

            а где в вашем варианте обработка ошибок?


            1. nin-jin
              04.01.2023 19:59

              А они сами обрабатываются, прикладнику о них думать постоянно не нужно.


              1. e_Hector
                04.01.2023 22:18

                А как именно обрабатываются?

                Что делать, если в зависимости от ошибки нужно показать какое-то сообщение пользователю?


                1. nin-jin
                  05.01.2023 00:21

                  Если надо что-то кастомное, то заворачиваем в try-catch и делаем всё, что захотим:

                  @mem pageTitle() {
                    
                    try {
                      return this.task().title()
                    } catch( cause ) {
                      
                      if( cause instanceof Promise ) {
                        return `Task #${ this.task().id() }`
                      }
                      
                      if( cause instanceof HttpError ) {
                        return {
                          2: `????`,
                          3: `????`,
                          4: `????`,
                        }[ Math.floor( cause.code / 100 ) ] || `????`
                      }
                      
                      throw cause
                    }
                    
                  }


      1. nin-jin
        03.01.2023 16:07

        в реатоме из коробки асинхронный трекинг контекста-стека, чего мобыксу скорее всего никогда не получить

        Это как?



  1. gev
    03.01.2023 13:31

    Сравнивали с Recoil?


    1. artalar Автор
      03.01.2023 13:37

      Не интересно, тк он прибит к реакту. Если это не смущает, лучше на jotai посмотреть - проще и легче рекоила.


      1. nin-jin
        03.01.2023 20:30

        Зачем же советовать заведомо плохое решение?


        1. artalar Автор
          03.01.2023 20:32

          Для кого-то перф не самое главное


          1. nin-jin
            03.01.2023 21:22

            Там и без перфа проблем хватает.


  1. maxfarseer
    03.01.2023 13:50

    Рад видеть развитие у Reatom. Продолжай!


  1. yroman
    03.01.2023 13:55

    Библиотека интересна, но пока не особо ясно как к ней подступиться. Поэтому вопросы:

    • Сколько разработчиков на проекте и какова вероятность, что вы забросите проект из-за каких-либо обстоятельств? Ежу понятно, что никто в здравом уме не станет тащить решение одиночки в коммерческие проекты и завязывать на это всю архитектуру.

    • Хотелось бы интересных примеров более серьезных приложений, где используется ваш подход. Очевидно, что приведенные в статье примеры, ммм, не слишком впечатляют. Хотелось бы лучших практик построения моделей, взаимодействия и так далее. У вас же должно быть видение? Для редакса и мобикса это все есть.


    1. artalar Автор
      03.01.2023 14:22

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

      Забрасывать его не планирую, этот проект мне очень дорог, но честно говоря сейчас бас фактор - я один.

      Реатом старается быть максимально примитивным: есть атомы для значений, есть экшены для логики - играйтесь. Дальше можно строить и ФП и ООП, впринципе свой фреймворк. Я предпочитаю простую процедурщину с минимум абстракций. При этом из-за обязательных принципов к иммутабельности, выделению эффектов в отдельный слой и еще пачке рекомендаций совсем жесть написать будет сложно, наивно код просто получается нормальным.

      Примеры больших проектов я считаю смотреть безсмысленно, у каждого своя специфика. Но вот в опенсурсе есть один такой: https://github.com/konturio/disaster-ninja-fe


      1. yroman
        03.01.2023 15:15

        Хм, а можно примеры таких компаний? Я хабр читаю давно, а вот про reatom услышал только недавно. Тот же моль уже здесь всем оскомину набил давно.

        Не могу согласиться насчет примеров проектов, все равно есть явные схожие паттерны, но за ссылку спасибо, посмотрю.


        1. artalar Автор
          03.01.2023 15:26

          За последние несколько лет вот эти компании точно использовали реатом: Яндекс, A3, sravni.ru, consta.design, и еще пачка о которых я не помню / не знаю. У самого сейчас reatom/async используется на проекте в пол ляма строк.

          Последние несколько лет реатом находился в постоянной разработке - поэтому я его сильно не пиарил. Текущая версия меня удовлетворяет достаточно, что бы на ближайшие пару лет сфокусироваться только на разработке экосистемы, теперь я могу смело говорить что это универсальное и надежное решение с чистой совестью.


    1. orekh
      04.01.2023 07:36

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


  1. nin-jin
    03.01.2023 15:13
    +6

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

    К чему приводит пресловутая атомарность селекторов можно наблюдать в этом примере: когда гуляешь по карте довольно скоро всё приложение замирает и перестаёт реагировать на пользователя вообще. А ведь год назад я объяснял почему такое поведение плохо. Особую пикантность ситуации придаёт отсутствие ошибок в консоли.


    1. artalar Автор
      03.01.2023 15:17

      Причем тут атомарность вообще не ясно. В любом случае, у меня ничего не замирает и потребление памяти вообще не меняется, "гулял" 1 минуту. Как воспроизвести проблему?


      1. nin-jin
        03.01.2023 15:20
        +2

        Поправил ссылку.


        1. artalar Автор
          03.01.2023 15:28

          Так что с ней делать?


          1. nin-jin
            03.01.2023 15:33
            +1

            Открыть и увидеть зависшее приложение через секунду использования.


            1. artalar Автор
              03.01.2023 15:39

              Не получается (( все работает, хотя жрет пол гига ОЗУ.


              1. yroman
                03.01.2023 15:42
                +1

                К сожалению, действительно виснет. Win 11, Firefox 108.0.1. Можно немного продвинуться по лабиринту, а потом всё.


              1. nin-jin
                03.01.2023 16:25

                stackblitz возможно что-то закешировал слишком агрессивно. Возможно поможет очистка локального хранилища.


                1. artalar Автор
                  03.01.2023 16:52

                  Воспроизвел. А что ты там сделал? Оригинальный сендбокс работает https://stackblitz.com/edit/effector-vite-react-template-3p6t2a?file=src%2Fmodel.ts, а по твоей ссылке твой форк.


                  1. nin-jin
                    03.01.2023 16:57

                    Добавил тривиальную багу. Счастливой отладки.


                    1. strokoff
                      03.01.2023 19:31
                      -3

                      Спасибо, за отличный пример. Пометил себе реатом в мертвооожденные. Обожаю хабр за это, один на серьёзных щах пердлагает ганвокодить по его идеологии, а комьюнити просто показывает, что поделка не работает и желает счастливой отладки! Лучше аргумента и не придумать, браво)


                      1. markelov69
                        04.01.2023 13:15
                        -3

                        Пометил себе реатом в мертвооожденные

                        Полностью поддерживаю

                        Обожаю хабр за это, один на серьёзных щах пердлагает ганвокодить по его идеологии

                        Во во, и самое смешное и печальное что остальные этот бред подхватывают


                    1. Pijng
                      03.01.2023 22:52

                      Вы же потом отпишите – в чем баг то был!


                      1. nin-jin
                        03.01.2023 23:11

                        Его не сложно найти с помощью отладчика. Типичный для чистой функции баг со внезапным NPE при некоторых входных значениях, которые не проверены в тестах.


                      1. Pijng
                        04.01.2023 16:00

                        В общем-то нашел, понятно – в $playerBlock индекс вне границ будет.
                        Энивей немного странно слышать комбинацию из "чистой функции" и "npe". Надеюсь курсивом не просто так выделено.

                        А что предлагают другие решения в таких ситуациях?



                      1. Pijng
                        04.01.2023 18:46

                        Понятно.

                        @artalar, какой есть идиоматический подход для кейсов, когда вычисления значений в атоме могут выкинуть исключение (ну то есть когда нарушили буквально второй абзац из доки после вступления)

                        Обратил внимание на ctx.schedule(), но не уверен, что это оно.


                      1. artalar Автор
                        04.01.2023 23:39

                        В самих вычислениях catch. делать не нужно, там впринципе намеренно никогда не должны исключения кидаться, соответственно и ловиться. Все кетчи над вызовом акшена / апдейта атома в асинхронном контексте (из ui-эвента или после await).

                        schedule нужен что бы планировать сайд-эффекты из экшенов.


      1. hapcode
        03.01.2023 15:41
        +1

        У меня тоже замирает. Секунд через 5.


  1. Alexandroppolus
    04.01.2023 00:52

    вы можете использовать spy в любом порядке (привет, правила хуков реакта). Вы можете применять его в условии и подписываться только на нужные атомы, когда это действительно актуально, что оптимизирует автоматически ваши потоки данных и помогает избавится от лишних вычислений.

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


    1. artalar Автор
      04.01.2023 05:18

      Это хорошее замечание, реатома пока так не умеет делать внутри компонента, но я работаю над этим (не думал что эта фича приоритетная).