Автор: Маслов Андрей, Front-end разработчик.
Время чтения: ~10 минут

Business logic with ease.

Содержание:

  1. О статье.

  2. Почему нужно использовать effector ?

  3. Концепция.

  4. Полезное и основное из api.

  5. Как работает ядро Effector, простым языком.

  6. Итоги.

  7. Полезные материалы.

О статье

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

Часть №1 будет нести ознакомительный характер c инструментом, чтобы вы могли понять, нужен ли вам Effector или нет. Разберем основные возможности и затронем то, как работает ядро библиотеки.

Почему нужно использовать Effector ?

  • Больше никакого бойлерплейт-кода.

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

  • Удобное и большое API, которое избавит разработчика от многих рутинных вещей.

  • Бизнес-логика теперь не "размазана" по файлам-контроллерам, а изолирована по процессам, интерфейсы не пересекаются с логикой (подобие реализации MV* паттернов).

  • Никакой магии, все построено на графах и подписках (об этом поговорим в конце этой статьи).

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

  • Постоянная поддержка, релизы с фиксами и новыми фичами.

  • Легковесность и скорость.

  • Поддержка TypeScript.

Концепция

Работу всего стейт-менеджера обеспечивает три основных юнита:

Store

Объект для хранения данных.

createStore - функция создания стора, название принято начинать со знака $.

Event

Этот юнит является главенствующей управляющей сущностью. С помощью event запускаются реактивные вычисления в приложении.

createEvent - функция создания события.

Вы можете подписать ваш store на какие-либо эвенты, при которых все зависимые от этого стора компоненты будут обновляться, сделать это можно при помощи .on, передав в метод первым аргументом - юнит, вторым - коллбэк функцию, которая будет возвращать результат изменения стора.

Пример:

//init.js

export const eventPlus = createEvent()
export const eventMinus = createEvent()

export const $storeCounter = createStore(0)
  .on(eventPlus, (store) => store + 1)
  .on(eventMinus, (store) => store - 1)

//components.jsx

export Component = () => {
  const count = useStore($storeCounter) 
    //От этого хука можно будет отказаться, 
    //при использовании effector/reflect (рассмотрим в последующих туториалах) 

  return (
    <h1>{ count }</h1>
  )
}

Согласитесь, выглядит очень лаконично и просто.

Effect

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

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

Пример:

//api.js

export const getCount = (payload) => {
  return axios.get('/count', payload)
}

//init.js

export const getCountFx = createEffect(getCount)

Effect предоставляет множество эвентов, например, doneData, failData, pending и тд. (Подробнее можно ознакомиться в документации).

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

//init.js

export const $count = createStore(0)
  .on(getCountFx.doneData, (_store, res) => res.data.count)

//components.jsx

export Component = () => {
  const count = useStore($count)  

  if (getCountFx.pending) {
    return <h1>Loading...</h1>
  }

  if (getCountFx.failData) {
    return <h1>Error</h1>
  }

  return (
    <h1>{ count }</h1>
  )
}

Полезное и основное из api

combine - позволяет комбинировать несколько сторов и создавать один производный.

Создадим стор, который будет хранить булево значение дизейбла кнопки submit, если запрос на получение счетчика в статусе "pending" или если этот запрос завершился в блоке catch. Третий аргумент является необязательным и служит для трансформации состояния.

const $submitDisabled = combine(
  getCountFx.pending,
  getCountFx.failData,
  (pending, faildData) => pending || faildData
)

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

Напишем код, который будет выводить ошибку.
Forward принимает объект с двумя полями: from и to, которые ожидают юниты (или массивы юнитов), при выполнении from вызовется юнит to.

const showErrorFx = createEffect(() => showToast('Something went wrong'))

forward({
  from: getCountFx.failData,
  to: showErrorFx
})

guard - метод, который позволяет запускать юниты по условию.
Напишем код, который будет отправлять запрос формы на бэкенд, если форма валидна.

guard({
  clock: sendEvent, //юнит, при срабатывании которого будет выполняться filter
  filter: $isValid, //дальнейший вызов target возможен при filter = true
  source: $form, //данные, которые будут передаваться в target
  target: submitFormFx // юнит, который будет вызван при вызове clock и истинном значении filter
})

sample - метод, принцип работы как у guard, добавляется аргумент fn - коллбэк, результат вызова которого будет передан в target.

sample({ source?, clock?, filter?, fn?, target?})

API Effector предоставляет большое разнообразие методов, выше вы видите лишь те, которые я использовал на проекте чаще остальных, и это малость из доступных, обязательно рассмотрите следующие методы: is, restore, split, attach. Так же почитайте про нерассмотренный ранее юнит domain.

Просто о том, как работает ядро Effector.

Основа - обход графа в ширину, где вершины графа являются событиями в очереди, которые хранятся в объекте ядра, выглядит так:

export type Node = {
  id: ID
  next: Array<Node>
  seq: Array<Cmd>
  scope: {[key: string]: any}
  meta: {[tag: string]: any}
  family: {
    type: 'regular' | 'crosslink' | 'domainn'
    links: Node[]
    owners: Node[]
  }
}

Рассмотрим три основных свойства объекта:

next - массив ребер графа (ссылки на следующие вершины)
seq - массив с последовательностью шагов
scope - объект данных, необходимый для работы шагов

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

Схема работы ядра. Обход графа.
Схема работы ядра. Обход графа.
Схема работы ядра. Обход графа.
Схема работы ядра. Обход графа.
Схема работы ядра. Обход графа.
Схема работы ядра. Обход графа.

К слову, это упрощенная модель работы эффектора. Так, например, очередей в ядре 5, а не 1. Все эти очереди отличаются приоритетом к выполнению.

Итоги

При первом использовании данной библиотеки у меня было двоякое чувство, ведь зачем мне отказываться от того же Redux или MobX, "и так все хорошо", лишь после полугода плотного использования effector на проекте я стал замечать, что разрабатывать стало проще и быстрее, а код стал структурированным и понятным.

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

Не стоит в комментариях заниматься холиварами, не воспринимайте статью, как единственный и истинный источник правды. Оставляйте комментарии по непоняткам, найдем правду вместе :D

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

Далее мы развернем реальное приложение в связке TypeScript + Effector (в документации все примеры на js, поэтому для новичков использование ts может стать не самым приятным делом), поговорим о feature-sliced архитектуре и best practices.

Материалы для закрепления:

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


  1. markelov69
    11.11.2022 17:43
    +4

    По вашему вот этот вырви глаз "код" и "подход", кодом этого конечно можно с большой натяжкой называть. Хоть на грамм может что-то сопоставить MobX'у? Да никогда, между ними пропасть в триллионы световых лет. Даже убогий Redux на фоне Effector выглядит гораздо лучше, т.к. там все гораздо очевидней.

    Если ваша цель запороть проект с самого начала, то да, возьмите Effector, пусть у всех взорвется мозг и будет взрываться дальше в геометрической прогрессии с добавлением каждой новой фичи и с каждым расширением функционала. С таким же успехом вы можете взять RxJS, результат будет одинаковым и НЕжизнеспособным.


    1. amas1ov Автор
      11.11.2022 22:03
      -1

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

      Хоть на грамм может что-то сопоставить MobX'у?

      У MobX (сейчас как раз на проекте используем) есть свои проблемы (как и у Effector, не пытаюсь боготворить). Например, неявные подписки на наблюдаемые изменения, из-за Proxy приходится писать постоянно модели данных (иначе консоль у тебя засыпается предупреждениями, и это не говоря про реактивный контекст). Store становится сборной солянкой из наблюдаемых, вычисляемых значений, экшенов и всего остального, если фича начинает разрастаться.

      В эффекторе же у вас есть только события, которые способны изменять стор.
      Effector будет полегче того же МобХ почти в 2,5 раза (говорю про связку с реактом).

      вырви глаз "код" и "подход"

      Подход, действительно, необычный. Я упоминал в статье, что первые полгода не мог привыкнуть. Про "вырви глаз код", здесь каждому свое, по мне так код effector один из наиболее приятных.


      1. DmitryKazakov8
        11.11.2022 22:59
        +3

        Не могли бы привести пример предупреждений в консоли? У меня нету ни в одном проекте, как вызвать?

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

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

        "В эффекторе же у вас есть только события, которые способны изменять стор" - ну и в Mobx есть функции-экшены, которые изменяют стор, только без необходимости иммутабельно писать как в редаксе (store, data) = > ({...store, items: store.items.map(item => ({...item, isViewed: data.id === item.id}))}) , разводя дубляж и значительно усложняя код, и заводя прослойку в виде событий, которые являются чистым бесполезным бойлерплейтом. Все, что нужно - вызвать функцию, она меняет нужный параметр в сторе, автоматически происходит эффективный перерендеринг.

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


        1. inoyakaigor
          12.11.2022 17:45
          +1

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


          1. markelov69
            12.11.2022 17:47

            import { configure } from 'mobx';
            
            configure({
                enforceActions: 'never',
            });

            Разумеется нужно делать так.


            1. inoyakaigor
              13.11.2022 10:52
              -1

              Вам видимо никогда не приходилось искать кто изменил какую-то переменную в сторе. Ставить костыли через itercept или того хуже spy. Иначе вы бы так не говорили))


              1. markelov69
                13.11.2022 11:23

                Вам видимо никогда не приходилось искать кто изменил какую-то переменную в сторе. Ставить костыли через itercept или того хуже spy. Иначе вы бы так не говорили))

                Ну в 99.99% случаях я знаю что делаю, когда пишу код) И знаю какая переменная когда и зачем должна изменятся и читаться, что при этом будет происходить и тп)
                А вообще всё просто, щелкаешь на переменную. правой кнопкой мыши и:
                WebStorm:

                VS Code:

                Там видно где переменная читается, где она мутируется. Если этого оказалось не достаточно для понимания того места, которое вы ищете, то опять же всё просто, на помощь приходит console.log. Весть процесс дебага в самом худшем сценарии занимает не более 5-10 минут. Но у меня например за 6 лет использования MobX приходилось не более 3-4 раз прибегать к такому дебагу.

                Поэтому я считаю на все 100000% оправданно вырубать enforceActions чтобы не засирать код. По той же причине (не засирать код) я и runInAction(() => {}) не использую после асинхронных вызовов если мне надо смутировать более 1 реактивной переменной, ибо раньше у меня был автоматический батчинг настроен всегда, а начиная с react 18 и он стал не нужен, там и так рендер не синхронный стал.


                1. DmitryKazakov8
                  13.11.2022 11:38
                  +2

                  Тоже пользуюсь Find Usages, дебаг очень простой. А чтобы не было ворнингов все эшены автоматом оборачиваются в action, а для батчинга runInAction, если после асинхронных вызовов. Я пока не считаю, что оправданно вырубать enforceActions - многое в коде зависит от батчинга (мне нужно чтобы несколько переменных менялись сразу, а не постепенно - не для рендера в Реакте, а для autorun, которые например отслеживают форму - надо чтобы и value поменялись, и isFocused, и валидаторы, и авторан вызвался 1 раз с финальными данными). И таких ситуаций много. Плюс я стараюсь делать библиотеко- и фреймворко-независимую архитектуру, а в альтернативах MobX тоже используются колбэки системной функции для батчинга, иначе его просто не выстроить синхронно.

                  К теме дебага - все экшены у меня еще логируются в визуальный интерфейс (история вызова + время на исполнение) в виде stack bars chart, что максимально сужает поиск по Find Usages.


                  1. markelov69
                    13.11.2022 12:30
                    -1

                    (мне нужно чтобы несколько переменных менялись сразу, а не постепенно - не для рендера в Реакте, а для autorun, которые например отслеживают форму - надо чтобы и value поменялись, и isFocused, и валидаторы, и авторан вызвался 1 раз с финальными данными). И таких ситуаций много.

                    Ладно, вот бесплатный лайфхак для таких целей) Если конечно конфиг с автобатчингом вас не устраивает)
                    Работает как часики)

                    function batchedAutorun(fn) {
                      let timeOut = null;
                      let firstRun = true;
                      function batch() {
                        if (firstRun) {
                          firstRun = false;
                          fn();
                        } else {
                          clearTimeout(timeOut);
                          timeOut = setTimeout(fn);
                        }
                      }
                    
                      return autorun(batch);
                    }

                    Тоже самое для reaction по аналогии.

                    Вот быстро проверить протестить - https://codesandbox.io/s/summer-leaf-hkv0x9?file=/src/index.js


                    1. DmitryKazakov8
                      13.11.2022 12:45
                      +1

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

                        configure({
                          reactionScheduler: f => setTimeout(f, 1)
                        });


                      1. markelov69
                        13.11.2022 14:10

                        Спасибо что скинули мой же конфиг) Только у меня чуть более усложнен) И в нем учитывается нюанс c controlled input'ами.

                        Если вам не нужны синхронные подписки - хорошо

                        Неа, не нужны, у меня всегда вся архитектура строится на том, что мы живем в асинхронном мире JS'a в браузере/на сервере)

                        синхронные изменения - это тоже очень важно

                        Изменения и так синхронные) А вот потребность именно в мгновенных синхронных реакциях на них, ну не знаю не знаю) Всё таки всё вращается вокруг асинхронности у нас)

                        Но всё же приведите пожалуйста реальный боевой пример, где нужны именно синхронные autorun/reaction с забатченными изменениями, а асинхронные autorun/reaction с забатченными изменениями не подойдут, т.к. не будут работать для достижения ваших реальных боевых задач. Я просто вот думал думал и ничего в голову не может прийти, но расширять кругозор нужно всегда и иметь в виду дополнительные кейсы всегда полезно


                      1. DmitryKazakov8
                        13.11.2022 17:51

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

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

                        3. Ну и минорное замечание к решению выше - если подменять одну функцию другой и передавать в setTimeout(fn) то контекст теряется и в стеке вызовов будет batch вместо реального имени функции. Фиксится парой строк, но все равно об этом думать надо

                        Оказывается, это Mazaa шифруется под новым ником, интересно)

                        В реальных задачах я могу вешать 5 авторанов последовательно, и даже если меняется их порядок - то где-то работает некорректно. Конечно, это антипаттерн и нужны прямые руки и продумывать лучше, но как это всегда бывает - нужно все срочно и потоки МР по 5-10к строк ежедневные, вангую, что с асинхронным вызовом было бы проблем больше, чем порядок поменять.


                      1. markelov69
                        13.11.2022 20:10
                        -1

                        По пунктикам:
                        1) Вот именно что все отработает синхронно при первом вызове, а дальше чтобы не повисало тоже решается легко с помощью проверки на if (SSR_MODE) { return autorun(fn) } else { вариант с автобатчингом }

                        Так что аргумент в "против" не засчитан, т.к. с этой задачей справляется на ура)

                        2)

                        Цепочки авторанов (в первом меняется параметр, который слушает второй авторан, в третьем прослушивается результат второго) 


                        Цепочка авторанов влияющих друга на друга это антипаттерн) Можно загнать реакции в бесконечный цикл)

                        Ну и тоже относится очень сильно к вашему специфическому подходу) Абсолютно прекрасно любые задачи реализуются с асинхронными реакциями ан самом деле)

                        Т.к. ориентируюсь на более-менее свободную замену частей приложения (в том числе рендерера типа Реакта)


                        Ну это тоже не аргумент, на практике никто и никогда не меняет view с реакта на vue/angular/svelte, без конкретного переписывания, понятное дело что на peact можно сменить почти(!) безболезненно. И вы всё равно никуда не убежите от специфики работы с тем или иным view фреймворком. Так что тоже не засчитывается) Ну серьезно, кейсы из области фантастики или 1/1000000 в расчет никогда не берутся, иначе вообще никогда и ничто нам не подойдет.

                        3) Тоже из области фантастики и на практике разумеется не применимо, ибо тогда либо вы не понимаете что вообще делаете в этой жизни или дизайн вашего приложения слетел с катушек)

                        В общем для 99.99% проектов и людей, кроме вас предложенный мной вариант автобатчинга более чем подходит)) Хотя на самом деле и вам подойдет, т.к. единственный рабочий аргумент был про SSR, но и он элементарно контрится, поэтому оказался нерабочим аргументом)


                      1. DmitryKazakov8
                        13.11.2022 21:58

                        Про "никто не меняет вью" в принципе согласен, но я скорее для спортивного интереса поддерживаю архитектурные части изолированными и абстрагированными. Не только Реакт могу поменять на Preact/Inferno/SolidJS и экспериментирую с заменой через адаптеры на другие рендереры (даже веб-компоненты), но и MobX на другие реактивные библиотеки, тот же Solid mutableStore. Это позволяет мне лично расти как специалисту и создавать долгоподдерживаемые проекты, заодно сравнивая перфоманс и набор фичей и оттачивая дизайн системы. И в целом стараюсь не завязываться на конкретные особенности (ex. версия определенной либы, которая рендерит асинхронно). И другие архитектурные части тоже делаю самодостаточными и независимыми. Поэтому хаки чисто для MobX в виде асинхронного автобатчинга не рассматриваю.

                        Конечно, можно и с ним жить, и утилиты подбить так, что будет и SSR, и нормальный стек вызовов, и соблюдена очередность реакций. Можно и на альтернативные библиотеки наворачивать такой функционал, но все равно мне ближе явный подход - где изменяется стор, оборачивать в batch (runInAction в mobx). Так добавляется бойлерплейт, но упрощается понимание за счет синхронности операций в и так непростом асинхронном мире JS, а в ряде случаев - улучшается перфоманс (незначительно, но в перегруженных компонентами страницах асинхронщина может привести к глитчам).

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

                        И еще - в асинхронной реакции может уже не быть той сущности, с которой она должна работать (компонент размаунтился, this = undefined, состояние модульного стора очистилось).

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


                      1. markelov69
                        13.11.2022 22:51
                        +1

                        У меня тоже своя реализация роутинга с помощью MobX'a, react-router не уважаю, но там пофиг синхронно reaction отрабатывает или асинхронно) Ну суть понятна, у вас больше тягота к синхронности, а мне по большому счету не принципиально, но по умолчанию все под асинхронность проектирую и закладываю, наверное поэтому и проблем не знаю и ни с чем не борюсь)


                    1. mayorovp
                      13.11.2022 18:15
                      +1

                      А ничего, что ваш batchedAutorun выполняется ровно 2 раза?


                      Замените в вашем примере setTimeout на setInterval и посмотрите что происходит...


                      Заканчивайте уже херню советовать.


                      1. markelov69
                        13.11.2022 18:59
                        -1

                        Это знак, что лучше избегать autorun и использовать reaction)

                        function batchedReaction(expression, fn, options) {
                          let timeOut = null;
                          function batch() {
                            clearTimeout(timeOut);
                            timeOut = setTimeout(fn);
                          }
                        
                          return reaction(expression, batch, options);
                        }


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

                        Заканчивайте уже херню советовать.

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


                      1. mayorovp
                        13.11.2022 20:08

                        Или же можно просто передавать { delay: 1 } в качестве options вместо нагораживания своих велосипедов...


                      1. markelov69
                        13.11.2022 21:10

                        Так не интересно, это слишком просто. А так да

                        autorun(() => {
                            // ...
                        }, { delay: 1 });

                        did the trick

                        Ну в любом случае с автобатчингом таких проблем и заморочек не имеешь и отвыкаешь от всех этих неудобств


                      1. konclave
                        14.11.2022 13:49

                        Досмотрел этот тред и после всех этих обсуждений типа "да тут надо подтюнить в конфигах и обернуть в все в функцию, которая будет пропатчивать что-то, чтобы ворнинги не сыпались" начальное утверждение "да mobx в тысячу раз проще" выглядит уже не таким однозначным =)


                      1. mayorovp
                        14.11.2022 14:49

                        На самом деле там всё просто, главное — не начинать читать комментарии с "добрыми" советами


                      1. markelov69
                        14.11.2022 15:28
                        -1

                        Это просто были рассуждения над специфическими кейсами @DmitryKazakov8 из-за его сильной любви к максимально возможной синхронности))

                        Для всех остальных(в том числе меня) всё предельно просто:

                        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);
                            },
                        });

                        И готово.
                        Это если вы не утопаете в легаси и используете React 18, а если ниже версия, то для input'ов реакция должна отрабатывать синхронно. Это тоже легко решается, просто за счет расширения React.createElement. Если актуально могу целиковое решения для React младше 18 версии приложить.


      1. markelov69
        11.11.2022 23:00
        +2

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

        Нет ни одной задачи во фронтенде, где React + MobX может не справится или может не подойти. За исключением тех, где нужно сэкономить каждую наносекнду и каждый байт памяти. Но тут в вэбе не разгуляться, только голый JS.

         и под конкретную команду.

        Тоже не совсем верно, т.к. если вашу вкусы скажем так специфичны в текущий момент времени, и потом вы увольняетесь/уходите на другой проект, то потом ваши так сказать вкусы должен будет расхлебывать кто-то другой. Есть большая проблема, одно дело код написан по простому, вот прям реально все просто, все читается слева направо и сверху вниз, заходишь в любой обработчик события и в нем же описано, что откуда мы взяли, куда что записали, куда какие запросы послали. Так вот, когда код написан так, то проблем вообще никогда не возникает, а вот когда вы начинаете накручивать лапшекод, запутываете элементарные вещи, притаскиваете всевозможный мусор типо effcotr'a, rxjs, xstate и т.д и т.п., то проект становится не жизнеспособным и способен хоть как-то худо бедно держаться на плаву, пока вы его поддерживаете.

        Например, неявные подписки на наблюдаемые изменения

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

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

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

        Store становится сборной солянкой из наблюдаемых, вычисляемых значений, экшенов и всего остального, если фича начинает разрастаться.

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

        В эффекторе же у вас есть только события, которые способны изменять стор.

        В эффекторе мы получаем максимально убогий, нечитаемый, неочевидный, лапшеобрзаный, неподдерживаемый, write-only код, нет спасибо.

        Effector будет полегче того же МобХ почти в 2,5 раза (говорю про связку с реактом).

        Очень смешно когда в качестве "аргумента" ставят десяток килобайт в бандле, который капля в море в реальных проектах. Ещё больше смешно, когда не называют конкретные цифры, а говорят в 2.5 раза, как бы приувиличивая значимость. Более того, на самом деле то, что в MobX кода минимально возможное количество, то опять же на реальном проекте эта ничтожная разница компенсируется и уходит в профицит.

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

        Пол года Крал, пол года. С MobX открываешь документацию, потом 3.5 примера и все. через 3 часа ты как рыба в воде. О чем это говорит? Правильно, о том, что там все супер просто, понятно и наглядно. Изменил переменную, компоненты которые ее читают перерендерились, точка. Финита ля комедия.

        Про "вырви глаз код", здесь каждому свое, по мне так код effector один из наиболее приятных.

        Оно и понятно собственно)

        Вот пример убогого и никчемного кода с MobX:

        // Что тут произходит? Не понятно же нифига!
        this.isFetching = true; 
        // Что тут произходит? Не понятно же нифига!
        this.items = await apiRequest('/some/data');
        // Что тут произходит? Не понятно же нифига!
        this.isFetching = false;

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

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


        1. funca
          11.11.2022 23:31
          +1

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

          Добавьте обработку ошибок, отмену ненужных запросов (через cancellationToken например) и какой-нибудь denounce, чтобы не реквестать слишком часто. Решение сразу будет выглядеть элегантно и красиво.


          1. markelov69
            12.11.2022 00:26

            отмену ненужных запросов (через cancellationToken например)

            Объективно это не нужная и бесползеная процедура. Это иллюзия отмены, т.е. для того кто не понимает как работает HTTP запрос и бэкенд будет казаться, что он якобы что-то отменил. Но для как это на самом деле:

            Браузер открывает TCP соединение, отсылает первые байты с данными в виде HTTP заголовков и тела запроса(если имеется), в это момент Nginx(или что угодно) проксирует этот запрос на бэкенд приложение(Node.js, PHP, C#, anyting else) и бэкенд начинает его обрабатывать, всё, обратного пути нет, но не может чудесным образом прерваться где-то в серединке. Он его обработает в любом случае целиком и полностью, но если вы его там прервется в браузере, то вы просто не получите ответа, любой супер тяжелый или нет запрос в БД улетит, отменяй/не отменяй это на клиенте. Так что это полная чушь и иллюзия для тех, кто не понимает как вообще работает бэкенд.

            Добавьте обработку ошибок, отмену ненужных запросов (через cancellationToken например) и какой-нибудь denounce, чтобы не реквестать слишком часто. Решение сразу будет выглядеть элегантно и красиво.

            fetchData = async () => {
              const canContinue = debouncer(this.fetchData);
              if (!await canContinue(500)) return false;
              
              this.isFetching = true;
            
              try {
                this.items = await apiRequest('/some/data');
                this.error = null;
              } catch (e) {
                this.error = e.message || 'Что-то пошло не так';
                console.error(e);
              } finally {
                this.isFetching = false;
              }
            }

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

            Надеюсь как реализовать функцию debouncer вы понимаете, но если нет, могу дополнительно поделиться реализацией, мне не жалко.

            Но если всё таки вы ярый фанат ненужных отмен, то это так же элементарно реализуется. И код будет выглядеть так:

            fetchData = async () => {
              const utils = asyncUtils(this.fetchData);
              if (!await utils.debounde(500)) return false;
              
              this.isFetching = true;
            
              try {
                /* 
                  Где utils.cancelToken = { value: null } т.к. нам нужно передать 
                  ссылку на объект чтобы в нее apiRequest записал токен для отмены
                */
                this.items = await apiRequest('/some/data', utils.cancelToken);
                this.error = null;
              } catch (e) {
                this.error = e.message || 'Что-то пошло не так';
                console.error(e);
              } finally {
                this.isFetching = false;
              }
            }

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


            1. Vadem
              12.11.2022 07:02
              +3

              Он его обработает в любом случае целиком и полностью, но если вы его там прервется в браузере, то вы просто не получите ответа, любой супер тяжелый или нет запрос в БД улетит, отменяй/не отменяй это на клиенте.

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

              Например, ASP.NET Core это может выглядеть примерно вот так:

              [HttpGet]
              public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken)
              {
                  await HeavyOperation1(cancellationToken);
                  cancellationToken.ThrowIfCancellationRequested();
                  await HeavyOperation2(cancellationToken);
              }

              Не везде и не всегда это имеет смысл, но забывать об этом не стоит.


              1. markelov69
                12.11.2022 10:07

                Самое страшное что вы правда в это верите и считаете применимым в реальности(( Будущее разработки в опасности(

                А теперь будем честны сами с собой - 99.99% Вообще не испытывают никаких нагрузок и вообще без разницы, отправили вы лишний запрос или не отправили. Даже самая дешманская виртуалка будет легко обрабатывать ~1000 RPS и больше при условии что вы специально не пишете супер медленный код, супер не оптимальные запросы и т.п. Так вот 99.99% в пике имеют порядка 50 RPS, а в среднем это число не доходит до 10.

                Теперь касаемо остальных: 0.01% в которых RPS хотя бы переваливает за 500, что тоже на самом деле является ничтожной цифрой с точки зрения вычислительных мощностей. А уж если они не развернуты на нормальной машине, так там не напрягаясь держаться тысячи и десятки тысяч RPS.

                1) Никто никогда не пишет и не будет писать разумеется бэкенд, который после каждой строчки проверяет отмену на клиенте. Просто представьте себе этот шматок Г, который ещё убьет свои производительность на ровном месте.
                2) Bottle neck это БД, а не бэкенд(c#, php и т.п.) что возводит в степень идиотизм в проверке на отмену.
                3) В базах данных нет никакой отмены. Расслабьтесь. Скормили ей тяжелый запрос, она будет его выполнять пока не закончить, т.к. он синхронный! И выполняется в отдельном потоке/процессе в зависимости от БД.
                4) Отменяй не отменяй, так же легко организовывается DDOS, которому пофиг на ваш говнокод с отменой на клиенте)


                1. funca
                  12.11.2022 11:52
                  +2

                  А теперь будем честны сами с собой

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

                  Не надо стесняться и оправдываться - пишите нормально, все будет нормально.


              1. Vadem
                13.11.2022 14:47
                +2

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

                После каждой строчки, очевидно, и не надо. Достаточно только перед началом каждой тяжёлой операции и/или её чанка. Часто это делается автоматически библиотеками/фреймворком.

                Просто представьте себе этот шматок Г, который ещё убьет свои производительность на ровном месте.

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

                2) Bottle neck это БД, а не бэкенд(c#, php и т.п.) что возводит в степень идиотизм в проверке на отмену.

                Запросов в БД может быть больше одного. Запросы в БД можно отменять. Кроме БД есть ещё очереди, файловые и объектные хранилища, вызовы API других сервисов и т.д. Всё это могут быть достаточно дорогие операции и лишний раз их выполнять не нужно.

                3) В базах данных нет никакой отмены. Расслабьтесь. Скормили ей тяжелый запрос, она будет его выполнять пока не закончить, т.к. он синхронный! И выполняется в отдельном потоке/процессе в зависимости от БД.

                В базах данных есть отмена. Например, PQcancel в PostgreSQL. Более того, результат запроса из базы возвращается обычно потоком и считывается драйвером постепенно. Чем раньше вы отмените запрос, тем меньше нагрузка на базу.

                4) Отменяй не отменяй, так же легко организовывается DDOS, которому пофиг на ваш говнокод с отменой на клиенте)

                Никто вроде и не говорил, что отмена запроса поможет как-то защититься от DDoS.


                1. markelov69
                  13.11.2022 15:51
                  -1

                  Сначала лирическая часть, относится к 99.99% всех вэб проектов:

                  Даже если вы через какие-то дикие костыли худо бедно, в каком-то виде сможете убивать/останавливать запросы в БД(именно чтобы БД перестала тратить ресурсы на выполнение конкретного запроса), это вообще никакой роли не играет от слова совсем, т.к. мы же говорим про 99.99% проектов, про реальность, а не вымышленную популярность и загруженность, RPS пиковый 20-50 это просто комарик на слоне, при условии просто одной обычной виртуалки самой стандартной. +-1000 RPS в зависимости того насколько вы умеете проектировать схему БД, писать запросы и т.п. вообще на изи переваривается этой же виртуалкой, проверено многократными тестами само лично и нагрузочными в рамках компаний в которых работал. А если реально шарите и знаете что БД должна находится на Dedicated сервере и обладать 100% русурсами железа и систему, то уж извините, тут уже десятки тыщ это просто как семечки. Так вот, о опять возвращаемся в реальность да, 99.99% это в пиках ну 50 RPS, ну так и быть в аномальный промежуток времени пускай 300 RPS, это просто ни о чем, это просто около нулевая нагрузка на базу. БД способна обслуживать тысячами запросы, десятками тысяч, а на современном желе, а не на том что из 2010ых так уже наверное и к стоням тысяч походят циферки.


                  Теперь часть для 0.01% проектов, да даже меньше конечно, 0.01% это преувеличение:

                  Ремарка, когда речь идет о по настоящему высоконагруженных проектах (средний RPS в течении суток хотя бы > 5 000 и пиковые всплекски > 25 000), то безусловно для них очень атуально экономить ресурсы и процессорное время, в том числе отменять запросы, вместо всякого дерьма типа ORM писать запросы нормально и т.д и т.п.

                  Далее речь о чем-то промежуточном:

                  Давайте тут порассуждаем уже конкретно.
                  1) Вот вы говорите тяжелая операция.
                  - Тяжелая это сколько по процессорному времени? 1 секунда? 1 милисекунда? 10 микросекнуд? Давайте определимся с точной цифрой для оценки этого понятия.

                  2) Какой RPS в этом вашем проекте, какой RPS был максимальный на каких-то из ваших проектах?

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

                  Просьба на второй вопрос не писать наглую ложь и ответить честно. Я то легко отвечу чество, за всю мою карьеру, на продакшенах самый пикой RPS который я видел на мониторинге это что-то около 40, но это прям кратковременно, а средней в течении суток - 2 с хвостиком, это максималка которую я наблюдал ан разных проектах. Если бы RPS был выше в 20 раз, бэкенд бы вообще не напрягаясь переварил. Но тут ещё ремарка, это всё про то как я пишу код, архитектуру строю и т.п. Есть люди и "технологии" у которых на 20 RPS всё загнется, без шуток, лично такое наблюдал, было и очень смешно и очень грустно одновременно. В IT гигантах типо гугл и яндекс не работал, да и как бы не хочу. Там своих заморочек айда ушел)


                  1. nin-jin
                    13.11.2022 17:20
                    +1

                    если вы через какие-то дикие костыли худо бедно, в каком-то виде сможете убивать/останавливать запросы в БД

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


                    1. markelov69
                      13.11.2022 17:28
                      -1

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

                      Да без разницы, путь даже поддерживают(в чем я сомневаюсь, что прям все там чики пуки и вот прям прерывают выполнение синхронного кода БД(конечно нет)), что с того-то? Какая вам и 99.99% с этого польза? Для RPS не достигающих хотя бы нескольких тысяч, вообще нет никакой разницы если вы разгрузите бэк благодаря абортам на 1-3% в лучшем случае.


                      1. nin-jin
                        13.11.2022 18:05

                        Я вам по секрету скажу, в нормальных фреймворках прикладнику об этом вообще думать не надо - оно само работает как надо.


            1. nin-jin
              12.11.2022 07:08

              запрос на бэкенд приложение(Node.js, PHP, C#, anyting else) и бэкенд начинает его обрабатывать, всё, обратного пути нет, но не может чудесным образом прерваться где-то в серединке

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


            1. funca
              12.11.2022 12:02

              Надеюсь как реализовать функцию debouncer вы понимаете, но если нет, могу дополнительно поделиться реализацией, мне не жалко.

              fetchData = async () => {
                const utils = asyncUtils(this.fetchData);
                if (!await utils.debounde(500)) return false;

              Для такого случая и правда интересно - как хранится состояние, ведь utils пересоздается при каждом вызове fetchData.


              1. markelov69
                12.11.2022 12:52
                +1

                fetchData = async () => {
                  const utils = asyncUtils(this.fetchData);
                  if (!await utils.debounde(500)) return false;

                Для такого случая и правда интересно - как хранится состояние, ведь utils пересоздается при каждом вызове fetchData.


                Неа, utils не пересоздается, а получает ссылку на один и тот же объект, вот подсказка:

                const asyncUtilsMap = new WeakMap();
                function asyncUtils(fn){
                    let utils = asyncUtilsMap.get(fn);
                    if (!utils) {
                        utils = {
                            debounce: getDeboundFn(fn),
                            noRaceConditions: getNoRaceConditionsFn(fn),
                            cancelToken: { value: null }
                        }
                        asyncUtilsMap.set(fn, utils);
                    }
                    
                    return utils;
                }


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


                1. funca
                  13.11.2022 01:33

                  function asyncUtils(fn){
                      let utils = asyncUtilsMap.get(fn)

                  Интересно, тут не будет проблем с методами как в примере выше? В JS используется наследование прототипов и один и тот же экземпляр функции будет разделяться разными оюъектами, которые этот класс породил.

                  class A {
                    fetchData() {
                       const untils = asyncUtilsMap.get(this.fetchData) 
                    }
                  }
                  let x = new A()
                  let y = new A()
                  x.fetchData === y.fetchData // true 

                  (по правде говоря я не проверял, но в теории это должно так работать).

                  Вызывая asyncUtils(this.fetchData) из разных объектов вы получите один и тот же экземпляр utils. Соответственно и состояние denounce внутри будет общим. Приятной отладки)

                  P.S. Я бы рад отвечать быстрее, но из-за всяких недалеких, обиженных жизнью,

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


                  1. markelov69
                    13.11.2022 08:33

                    Интересно, тут не будет проблем с методами как в примере выше? В JS используется наследование прототипов и один и тот же экземпляр функции будет разделяться разными оюъектами, которые этот класс породил.

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

                    fetchData = async () => {...}

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

                    class A {
                      fetchData() {
                         const untils = asyncUtils(this, 'fetchData or any unique key for this class') ;
                      }
                    }

                    Тут this у каждого экземпляра свой собственный, отсюда и коллизий не будет. А универсальная реализация asyncUtils будет выглядить так:

                    const asyncUtilsProtoMap = new WeakMap();
                    function asyncUtilsProto(context, key){
                        let utils = asyncUtilsProtoMap.get(context);
                        if (!utils) {
                            utils = {};
                            asyncUtilsProtoMap.set(context, utils);
                        }
                    
                        if (!utils[key]) {
                            const target = {};
                            utils[key] = {
                                debounce: getDeboundFn(target),
                                noRaceConditions: getNoRaceConditionsFn(target),
                                cancelToken: { value: null },
                            }
                        }
                    
                        return utils[key];
                    }
                    
                    const asyncUtilsArrowMap = new WeakMap();
                    function asyncUtilsArrow(fn){
                        let utils = asyncUtilsArrowMap.get(fn);
                        if (!utils) {
                            utils = {
                                debounce: getDeboundFn(fn),
                                noRaceConditions: getNoRaceConditionsFn(fn),
                                cancelToken: { value: null },
                            }
                            asyncUtilsArrowMap.set(fn, utils);
                        }
                    
                        return utils;
                    }
                    
                    
                    function asyncUtils(context, key = null){
                        if (key) return asyncUtilsProto(context, key);
                    
                        return asyncUtilsArrow(context);
                    }

                    У вас манера высказываться довольно специфическая, хотя как специалист вы наверное хороший.

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


                1. mayorovp
                  13.11.2022 18:24
                  +1

                  P.S. Я бы рад отвечать быстрее, но из-за всяких недалеких, обиженных жизнью, которые заминусовали карму из-за того, что мое мнение не сходится с их мнением, приходится писать не чаще 1 раза в час.

                  Проблема-тио не во мнении, а в тоне.


          1. nin-jin
            12.11.2022 07:02
            -3

            Добавьте обработку ошибок, отмену ненужных запросов (через cancellationToken например) и какой-нибудь denounce, чтобы не реквестать слишком часто. Решение сразу будет выглядеть элегантно и красиво.

            @mem // cache and automatic dispose
            items() {
              
              sleep( 500 ) // debounce
              
              try {
                
                return apiRequest( '/some/data' ) // automatic request cancellation
              
              } catch( error ) { // handle any error
                if( error instanceof Promise ) throw error // suspense api
                
                console.error( error )
                return [] // fallback
                
              }
              
            }


        1. amas1ov Автор
          11.11.2022 23:39
          +1

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

          Ввел в заблуждение знаком препинания, прокси и остальную часть предложения нужно расцепить.
          Вообще посыл ответа был таков, что нам приходится трястись над данными, например, обмазываться toJS
          Я с МобХ работаю не так давно, и не могу давать какое-то "экспертное" мнение, вы в этом явно лучше.

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

          Ваша проблема решается минутой гугления, не из головы взял

          С MobX открываешь документацию, потом 3.5 примера и все

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

          А $ в названиях переменных так это вообще вишенка на торте

          Это рекомендации, как и название интерфейсов или представлений компонент и тд.

          Напомню, что в начале статьи написал следующее (Не было цели сравнивать что-то, цель - ознакомительный характер, а ваши комментарии начинают нести придирчивый характер):

          Часть №1 будет нести ознакомительный характер c инструментом, чтобы вы могли понять, нужен ли вам Effector или нет...


          1. mayorovp
            13.11.2022 18:37

            С пониманием подхода на MobX все очень просто: любой наблюдатель подписывается на те свойства, к которым обращается. И он подписывается строго сам, никакие там дочерние или родительские компоненты автоматически никуда не подписываются.


            Соответственно, чтобы всё работало, нужно помнить следующее: любое обращение к реактивному свойству должно происходить строго внутри computed, autorun, reaction или observer. Если нужно прочитать значения из реактивной модели где-то еще — значит, надо или добавить autorun/observer, или же "доверить" чтение кому-то ещё. Видимо, потому вы и говорите об "обмазывании" toJS.


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


            1. Alexandroppolus
              13.11.2022 20:20

              любое обращение к реактивному свойству должно происходить строго внутри computed, autorun, reaction или observer.

              Наблюдаемое может понадобиться в колбэке какого-нибудь пользовательского события/таймаута/етц. Просто значение "на данный момент", один раз. Потому опцию observableRequiresReaction считаю ненужной.

              "Обратная" ей опция reactionRequiresObservable в общем полезна, но иногда может ругаться зря, когда observer-компонент при некоторых условиях таки не читает наблюдаемые.


        1. nin-jin
          12.11.2022 06:52
          -1

          Нет ни одной задачи во фронтенде, где React + MobX может не справится или может не подойти.

          • Виртуализированный рендеринг сложных интерфейсов.

          • Редактор документов, электронных таблиц и тп вещей.

          • Растровая и 3д графика.

          • Даже просто создание кастомизируемой библиотеки виджетов - та ещё боль на Реакте.


          1. funca
            13.11.2022 01:07

            Растровая и 3д графика.

            React Native? Мне казалось что он как раз чем-то таким и занимается.

            Для canvas https://habr.com/ru/post/276585/

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


            1. nin-jin
              13.11.2022 06:37
              -1

              Это всё из оперы "троллейбус из буханки хлеба". Можете сами скачать, например, приложение Самоката на RN и полюбоваться тормозящими анимациями с задержкой в несколько секунд.


      1. funca
        11.11.2022 23:17

        Mobx это умный стор для тупых клиентов. Redux-like предлагают наоборот - примитивный стор для умных.

        Пока логики немного, первый вариант выигрывает. Но развивая подход, вся сложность оказывается сконцентрированной в одном месте и оно становится узким. Дирежировать квартетом и дирежировать оркестром из 1000 инструментов это принципиально разные по сложности задачи.

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


        1. nin-jin
          12.11.2022 07:13
          +2

          MobX - это не стор, а реактивная система, связывающая состояния. Стор на MobX - это MST.


          1. funca
            12.11.2022 11:39
            -1

            MST это state - компонент стора. Но спасибо за коммент, терминология это тоже важно.


        1. markelov69
          12.11.2022 11:47
          -2

          Пока логики немного, первый вариант выигрывает. Но развивая подход, вся сложность оказывается сконцентрированной в одном месте и оно становится узким. Дирежировать квартетом и дирежировать оркестром из 1000 инструментов это принципиально разные по сложности задачи.

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

          Мда, вот это "логика", тяжелый случай.

          Единственное, в разработке требуется дисциплина (лучше что-нибудь из области математики)

          Ага, а ещё лучше из ядерной физики в перемешку с астрологией и битвой экстрасенсов.

          Но развивая подход, вся сложность оказывается сконцентрированной в одном месте и оно становится узким

          Это где? В вашем выдуманном мире? Если вы сами ее сконцентрируете в одном месте, она там будет, если нет, то нет. Можете размазать, можете не размазать и т.п. Это вы пишете код, а не mobx, effector, redux и т.п. Как вы сами напишете, ровно так и будет. Отсюда все "беды", во всем в первую очередь виноваты кривые ручки, а во вторую уже выбранные технологии и подходы.

          Effector предоставляет неплохой набор инструментов для такой композиции.

          Увы и ах, нет.


        1. mayorovp
          13.11.2022 18:43

          Пока логики немного, первый вариант выигрывает. Но развивая подход, вся сложность оказывается сконцентрированной в одном месте и оно становится узким.

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


          Нет ни единой причины сваливать всю логику ни в один класс, ни в один стор.


      1. nin-jin
        12.11.2022 06:45

        Effector будет полегче того же МобХ почти в 2,5 раза (говорю про связку с реактом).

        MobX: 16.3 kB + 2.2 kB = 18.5 kB

        Effector: 10.4 kB + 3.7 kB = 14.1 kB

        $mol_wire: 6.6 kB + ~1.5 kB = 8 kB


    1. Ashot
      12.11.2022 00:09
      +2

      Если ваша цель запороть проект с самого начала, то да, возьмите Effector

      С таким же успехом можно "запороть" проект redux'ом. Или mobx'ом. Или чем угодно. Так что я бы не стал так уж в лоб говорить о "запороть"(эффектором не пользовался, если что)


  1. nin-jin
    11.11.2022 19:24
    +1

    Легковесность и скорость.

    https://github.com/artalar/reactive-computed-bench

    Больше никакого бойлерплейт-кода.

    //api.js
    
    export const getCount = (payload) => {
      return axios.get('/count', payload)
    }
    
    //init.js
    
    export const getCountFx = createEffect(getCount)
    
    export const $count = createStore(0)
      .on(getCountFx.doneData, (_store, res) => res.data.count)
    
    //components.jsx
    
    export Component = () => {
      const count = useStore($count)  
    
      if (getCountFx.pending) {
        return <h1>Loading...</h1>
      }
    
      if (getCountFx.failData) {
        return <h1>Error</h1>
      }
    
      return (
        <h1>{ count }</h1>
      )
    }

    vs

    //api.js
    export class API extends Object {
      @mem count() { return fetchJSON( '/count' ).data.count }
    }
    
    //counter.jsx
    export class Counter extends Component< Counter > {
      count(){ return API.count() }
      vdom() { return <h1>{ this.count() }</h1> }
    }

    все это перекрывается прекрасным комьюнити, которое всегда готово прийти на помощь, например, в tg канале

    Или забанить, если будешь задавать глупые вопросы.


    1. amas1ov Автор
      11.11.2022 22:19

      Больше никакого бойлерплейт-кода.

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

      Redux

      //actions.ts - Определить экшен
      
      const someAction = function (props) {
        return {
          type: "SOME",
          props
        }
      };
      
      //reducer.ts - обработать экшен и изменить стор
      
      const reducer = function(state, action) {
        switch (action.type) {
          case "SOME":
              return ...
        }
        return state;
      }
      
      //Component.tsx
      
      const Component = () => {
        const dispatch = useDispatch()
      
        const handleSome = () => {dispatch(someAction())}
      
        return (...)
      }

      vs Effector

      //init.ts
      
      const someAction = createEvent()
      
      const $someStore = createStore()
                            .on(someAction, (state) => state)
      
      const Component = () => {
      
        const handleSome = () => {someAction}
      
        return (...)
      }

      И это мы в редасе еще стор сам не создавали, initialState не описывали и тд )
      Думаю, вы меня поняли.

      Про бан ничего сказать не могу, очень грустно, если это было просто за глупый вопрос.

      Спасибо за комментарий.


      1. nin-jin
        12.11.2022 05:49
        +1

        Я вам привёл эквивалентный код, который действительно без бойлерплейта. А вы сравниваете один боилерплейт с другим.


    1. funca
      11.11.2022 22:53
      +1

      Больше никакого бойлерплейт-кода.

      Смотрю в код тестов по вашей ссылке

      Effector

      const entry = createEvent<number>()
      const a = createStore(0).on(entry, (state, v) => v)
      const b = a.map((a) => a + 1)
      const c = a.map((a) => a + 1)
      const d = combine(b, c, (b, c) => b + c)
      const e = d.map((d) => d + 1)
      const f = combine(d, e, (d, e) => d + e)
      const g = combine(d, e, (d, e) => d + e)
      const h = combine(f, g, (h1, h2) => h1 + h2)

      mol

      const entry = new $mol_wire_atom('entry', (next: number = 0) => next)
      const a = new $mol_wire_atom('mA', () => entry.sync())
      const b = new $mol_wire_atom('mB', () => a.sync() + 1)
      const c = new $mol_wire_atom('mC', () => a.sync() + 1)
      const d = new $mol_wire_atom('mD', () => b.sync() + c.sync())
      const e = new $mol_wire_atom('mE', () => d.sync() + 1)
      const f = new $mol_wire_atom('mF', () => d.sync() + e.sync())
      const g = new $mol_wire_atom('mG', () => d.sync() + e.sync())
      const h = new $mol_wire_atom('mH', () => f.sync() + g.sync())

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


      1. nin-jin
        12.11.2022 07:19

        Если что, на $mol так не пишут. $mol_wire_atom - низкоуровневая абстракция, эквивалентная остальным решениям в этом бенчмарке. Типовой код на $mol_wire можно глянуть, например, тут.


        1. funca
          12.11.2022 10:09

          Если что, на $mol так не пишут.

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

          Кстати, а как там должно быть написано, чтобы это стало похоже на правду?


          1. nin-jin
            12.11.2022 12:04
            +1

            Да никакой. Бенчмарк этот довольно далёк от реальности. Там замеряется кейс вида "есть сложный DAG с тривиальными формулами, и на каждый чих они все пересчитываются". Это худший случай для $mol_wire и MobX и лучший для Effector и S.js. Но даже в этом случае, эффектору не удаётся конкурировать, в отличие от S.js. На более-менее реалистичном кейсе с кучей данных, тяжёлыми вычислениями и динамическими зависимостями эффектор просто встанет колом.

            Вот, кстати, более свежие результаты на моей машинке:


            1. funca
              12.11.2022 12:38

              Интересно какие результаты для $mol_wire_solo (насколько я понимаю это идиоматическая реализация как на практике). В последнем коммите этот код закомментирован.


              1. nin-jin
                12.11.2022 13:40
                +2

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



                1. funca
                  13.11.2022 00:39

                  Спасибо, теперь выглядит понятнее.

                  взаимный прогрев одной библиотеки

                  Согласен. Было бы удобнее если бы тесты для разных библиотек находились в разных модулях и запускались независимо друг от друга.


                  1. nin-jin
                    13.11.2022 06:32
                    -2

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


    1. kai3341
      12.11.2022 16:04
      +2

      Не могли бы вы объяснить, каких попугаев меряет бенчмарк по оси Y? От дополнительной информации касательно оси X тоже не откажусь


      1. markelov69
        12.11.2022 16:13
        +1

        Это бестолковые "бенчмарки", не относящиеся к реальным проектам и реальной логики приложений. Цикл "for in" быстрее чем цикл "for of", но в реальных клиентских вэб приложениях вы никогда не заметите экономии в микросекнудах и наносекундах. Тут нужно опираться только на то, какой код вам позволяет писать та или иная библиотека. Это важнее на порядке, чем то, на сколько надо секунд X обгонит Y. Тем более речь о клиентском приложении, а не о сервере.


      1. nin-jin
        12.11.2022 18:53

        По Y отставание от лидера (100%). Сомнительная метрика, конечно.


  1. noodles
    12.11.2022 12:17
    +3

    лишь после полугода плотного использования effector на проекте я стал замечать, что разрабатывать стало проще и быстрее

    Т.е. судя по всему, первые полгода было не проще и не быстрее. А потом подпривыкли, освоились в проекте, научились в "effector" - и дело пошло. Вопрос в том, что следующему разработчику надо будет теже полгода + время на вкуривание уже написанного кода, и только потом дело пойдёт.

    Всё-таки выборка для оценки должна быть побольше, например три проекта на мобх, и три проекта на эффекторе. При чём не так, что начинал с нуля, а потом уволился (потому что говорят что надо "развиваться" и прыгать с места на место). А наоборот, чтоб пришёл на проекты уже которые в возрасте.

    Мне достался двухлетний проект на mobx-е, он большой (при чём не вплане формочек, там только одна форма на входе, а вплане накрученной логики, там и sip + web-rtc, и electron). Всё супер понятно, уже четвёртый год пошёл проекту, а он развивается быстрее всех, с той же скростью что и была в начале, при этом без увеличения энтропии и wtf-индекса.. вся сложность упразднена. Порой такие требования прилетают, что кажется если бы не mobx - проект бы уже переписывали или закрыли. И самое классное - ни одного простигосподи usecallback-а, useMemo, memo в реакте. И только один реактовский контекст на всё приложение. Реакт чисто как шаблонизатор. Вобщем очень удобно.


    1. markelov69
      12.11.2022 13:56
      +1

      Мне достался двухлетний проект на mobx-е, он большой (при чём не вплане формочек, там только одна форма на входе, а вплане накрученной логики, там и sip + web-rtc, и electron). Всё супер понятно, уже четвёртый год пошёл проекту, а он развивается быстрее всех, с той же скростью что и была в начале

      Всё правильно, так и должно быть, вот что бывает когда адекватные люди разрабатывают и во главе всего стоит "KISS (Keep it simple, stupid)YAGNI (You aren't gonna need it)Чем меньше, тем лучше (Less is more)".

      Порой такие требования прилетают, что кажется если бы не mobx - проект бы уже переписывали или закрыли

      Так и случается в 100% случаев)


    1. amas1ov Автор
      12.11.2022 14:02
      +1

      Все от сложности проекта зависит, верно подметили.

      Вопрос в том, что следующему разработчику надо будет теже полгода + время на вкуривание уже написанного кода, и только потом дело пойдёт.

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

      Всё-таки выборка для оценки должна быть побольше, например три проекта на мобх, и три проекта на эффекторе. При чём не так, что начинал с нуля, а потом уволился (потому что говорят что надо "развиваться" и прыгать с места на место). А наоборот, чтоб пришёл на проекты уже которые в возрасте.

      Соглашусь.
      У меня с МобХ первый проект сейчас, поэтому никакой конкретики не приводил, плевки в сторону других решений не делал. Вкатился быстро, да так же как и можно вкатиться в проект на эффекторе (если рядом тебя за ухо поднатаскают), единственное отличие - у эффектора инструментарий будет пошире (и в ssr и в байндинг пропсов (никаких тебе вызовов юзстора в компонентах), ну и тд, следующую статью по инструменталу как раз готовлю), в этом и сложность для многих.

      Спасибо за комментарий )


      1. DmitryKazakov8
        12.11.2022 14:28
        +2

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

        import { observer } from 'mobx-react';
        import { Component } from 'react';
        
        import { TypeGlobals } from 'models';
        
        import { StoreContext } from './StoreContext';
        
        export class ConnectedComponent<TProps = any, TRoute = never> extends Component<TProps, any> {
          // SSR not able to recalculate components on observable updates,
          // so on server side it has to be rendered twice
          static observer = IS_CLIENT ? observer : (someComponent: any) => someComponent;
        
          // Describe context, so no boilerplate in components needed
          static context: TypeGlobals;
          static contextType = StoreContext;
          declare context: TypeGlobals;
        }
        
        // Any component
        
        class AnyComponent extends ConnectedComponent<TypeProps> {
          render() {
            this.context // Perfectly typed and reactive, no boilerplate at all
            this.props // Perfectly typed and reactive, no boilerplate at all
          }
        }

        В Webpack делаю транформер, который все компоненты, которые экстендят ConnectedComponent превращаются в

        const AnyComponent = ConnectedComponent.observer(class AnyComponent...)

        Так получается реактивность без сахарных декораторов (чтобы использовать не Babel, а более шустрый SWC транспайлер), с отдельной логикой для SSR и без бойлерплейта.

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


        1. amas1ov Автор
          12.11.2022 14:45

          Отлично , спасибо!