company_banner

Опыт использования MobX в большом приложении



    Всем привет!


    Меня зовут Сергей, я работаю в команде разработки приложений контроля качества Tinkoff.
    Поделюсь опытом нашей команды в использовании библиотеки Mobx и расскажу о деталях работы с ней в связке с React. В этой статье не будет описания базовых концепций. Я расскажу о вещах, которые мы отметили для себя за время разработки и считаем полезными для всех, кто решил использовать Mobx в своем проекте.


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


    Сказ об удобстве


    В статьях и комментариях о Mobx часто встречается слово «магия». Это связано с непониманием того, как Mobx обновляет UI-компоненты.


    На проектах с Redux, как правило, используют PureCompoment, хуки и shouldComponentUpdate для предотвращения лишних перерисовок. Mobx же автоматически переопределяет shouldComponentUpdate и использует мемоизацию для компонент. Скрытые под капотом оптимизации смущают разработчиков, а смущение вызывает недоверие. Чтобы понять, как работает MobX с React компонентами, я расскажу о проблеме, с которой мы столкнулись при использовании собственного HOC (Higher-Order Component), отвечающего за необходимость отрисовки компонента.


    Допустим, HOC называется withVisible и добавляет передаваемому компоненту дополнительный булевый параметр visible. Отрисовка компонента вызывается, если передать этот параметр со значением true, в противном случае возвращаем null:


    function withVisible(wrappedComponent) {
       function Visible({ visible, ...otherProps }) {
           if (!visible) return null;
           const WrappedComponent = wrappedComponent;
           return <WrappedComponent {...otherProps} />;
       }
    
       return Visible;
    }

    • Такая обертка делает компоненты чище, в render-функции больше не нужно описывать кейс, когда отрисовка не требуется.
    • Мы получаем единый инструмент для разруливания видимости переиспользуемых «глупых» компонент, таких как кнопки и инпуты.
    • Мы не любим тернарные выражения в JSX-коде. Благодаря withVisible HOC от них можно избавиться.

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


    const Loader = withVisible(LoaderComponent);
    const Audio = withVisible(AudioComponent);
    
    function Player({ model }) {
       return (
           <div>
               <Loader visible={model.isLoading} />
               <Audio visible={!model.hasAudio && !model.hasErrors} />
           </div>
       );
    }
    
    export inject("model")(observer(Player))

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


    Начнем по порядку.


    Передаваемая модель содержит три наблюдаемых значения: hasAudio, isLoading и hasErrors. Чтобы компонент обновлялся после изменения этих полей, используется функция observer из библиотеки mobx-react. Она подписывает компонент на обновления всех observable. Отметим, что эти поля напрямую не влияют на отрисовку компонента Player, но используются для отображения дочерних компонентов. Из-за HOC мы получили дополнительную перерисовку компонента на уровень выше того, состояние которого обновляется.


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


    В примере с кодом присутствует и другая проблема, которая не связана с HOC. Давайте взглянем на вот эту строчку:


    <Audio visible={!model.hasAudio && !model.hasErrors} />

    Это участок кода приводит к излишним вызовам render функции компонента Player. Как мы уже знаем, Mobx подписывает компонент на изменения hasAudio, isLoading и hasErrors, но на отображение Audio влияет результат выражения !hasAudio && !hasErrors. Этот компонент будет отображаться только в одном случае, если оба параметра будут равны false. При трех других возможных комбинациях значений этих полей HOC вернет null. Заметим, что родительский компонент Player будет обновляться при каждом отдельном изменении обоих из полей, но это не будет влиять на результат отрисовки.


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


    Чтобы не попадать на такие ошибки стоит придерживаться базового принципа, который советует создатель библиотеки Michel Weststrate.


    Базовый принцип


    Базовый принцип гласит: делайте компоненты как можно меньше и используйте computed-значения.


    Давайте разбираться. С разбиением на маленькие компоненты вроде бы все понятно: чем меньше компоненты, тем точечней будут перерисовки. Но зачем computed-значения? А они как раз помогают избежать ситуации, которая описана в проблеме выше:


    @computed
    get showAudio() {
           return !this.hasAudio && !this.hasErrors;
    }

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


    Computed автоматически вычисляется, если изменяется какое-либо observable-значение, влияющее на него. При изменении hasAudio или hasErrors showAudio получит уведомление и выполнит пересчет собственного значения. Возвращаемое значение кэшируется и не будет изменено, пока не поступит новое уведомление об изменении. То есть вычисления происходят не при каждом обращении к computed, а когда изменяются зависимые observable-поля.


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


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


    А всегда ли нужен хук useCallback?


    В классической архитектуре большого приложения, в котором используется React в связке с MobX, можно выделить три основных слоя:


    1. UI-представление;
    2. Модель с состоянием;
    3. Cервис.



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


    Рассмотрим пример. Компонент Button принимает в параметр onClick коллбек doSomething из модели Model.


    // Пример модели
    class Model {
       @action
       doSomething() {
           // some code
       }
    }
    
    // Пример компонента
    function AnyComponent({ model }) {
       return (
           <div>
               <Button onClick={model.doSomething} /> // не хорошо
               ...
           </div>
       );
    }

    Передать коллбек напрямую нельзя, теряется контекст. Чтобы решить эту проблему, разработчики пользуются тремя стандартными способами.


    Первый способ очень распространен в различных примерах с кодом и выглядит примерно так:


     <Button onClick={() => model.doSomething()} />

    Этот способ не оптимален. Использование стрелочной функции в render-методе приводит к созданию новой функции при каждом обновлении компонента. Button будет перерисовываться при каждом изменении родительского компонента из-за изменения ссылки в параметре onClick. Это нарушает оптимизации приложения и влияет на его производительность.


    Второй способ — сделать компонент классовым и вынести коллбек-функцию в отдельный метод класса при помощи стрелочной функции.


    class AnyComponent extends Component {
       onClick = () => {
        const { model } = this.props;
           model.doSomething();
       };
    
      render() {
           return (
               <Button onClick={this.onClick} />
           );
       }
    }

    Это стандартное решение. При передаче метода дочернему компоненту сохраняется контекст, но при этом функция не изменяет ссылку при обновлении компонента.


    C появлением хуков в React 16.8 появился еще один способ решения проблемы. Переделывать функциональный компонент на классовый из-за одного маленького коллбека перестало быть необходимым. Можно воспользоваться хуком useCallback:


    function AnyComponent({ model }) {
       const onClick = useCallback(() => {
           model.doSomething();
       }, []);
    
       return (
           <Button onClick={onClick} />
       );
    }

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


    Однако в mobx реализован еще один способ для передачи коллбека в дочерний компонент, но он подходит только для действий, которые изменяют observable-поля. Чтобы не использовать перечисленные выше решения, которые добавляют ощутимое количество кода, декоратор @action содержит дополнение в виде @action.bound. Оно, как и хук useCallback, возвращает мемоизированную версию коллбека и сохраняет контекст функции.


    При использовании @action.bound код будет выглядеть примерно так:


    // Пример модели
    class Model {
       @action.bound
       doSomething() {
           // some code
       }
    }
    
    // Пример компонента
    function AnyComponent({ model }) {
       return (
           <div>
               <Button onClick={model.doSomething} />
               ...
           </div>
       );
    }

    Заметим, что использование декоратора @action.bound делает код более компактным, но не эквивалентно применению хука useCallback. Не используйте его, если внутри коллбека нет изменения наблюдаемых полей.


    Строгость наше всё


    Как мы знаем, в MobX есть наблюдаемые (observable) переменные и action-функции, которые предназначены для изменения наблюдаемых переменных. Изменять observable-поля только из action-функций — хорошая практика, но не обязательная. Чтобы включить строгий режим для всего приложения, в mobx есть настройка enforceActions. Ее достаточно указать в корне проекта.


    import { configure } from "mobx"
    
    // don't allow state modifications outside actions
    configure({ enforceActions: "always" })

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


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


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


    import { observable, configure, runInAction } from 'mobx';
    
    class User {
       @observable surname = '';
    
       @observable lastName = '';
    
       @action
       fetchUser() {
           fetchRequest().then(
               data => {
                   runInAction(() => {
                       this.surname = data.surname;
                       this.lastName = data.lastName;
                   });
               }
               // ...
           );
       }
    }

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


    Как вариант, подойдет декоратор @action.bound, но придется выносить функцию обратного вызова в отдельный метод. Возможно, это один из самых простых и элегантных способов решения проблемы. С одной стороны, мы избавимся от ада обратных вызовов и получим более читаемый код за счет разбиения логики на модули, а с другой — придется соблюдать строгий подход, при котором на каждый коллбек асинхронной функции необходим отдельный метод, обернутый в action. А ведь хочется иметь возможность передавать анонимную функцию и не думать о декораторах на уровне обратного вызова.


    В библиотеке Mobx есть функция flow для работы с асинхронными action-функциями. Про нее можно прочитать тут, но я думаю, что она не подходит для многих проектов. Если не вдаваться в подробности, то ее основной минус в том, что работает только с генераторами. Согласитесь, далеко не во всех проектах используются генераторы и хочется писать всем привычные async-функции и промисы. К сожалению, в Mobx других альтернатив нет, но у нас на проекте было придумано свое решение.


    Для нашего приложения мы используем babel, и именно он позволил решить проблему с асинхронными функциями. Для него есть очень полезный плагин @babel/plugin-transform-async-to-generator, который при компиляции исходного JS-кода меняет все асинхронные функции на генераторы. А ведь с генераторами mobx умеет работать, осталось только это связать:


    "plugins": [
                    ["@babel/plugin-transform-async-to-generator", {
                        "module": "mobx",
                        "method": "flow"
                      }],
                ]

    В примере показана упрощенная настройка плагина для babel, которая заменяет каждую асинхронную функцию в генератор и оборачивает результат в mobx.flow. Однако такая замена может быть избыточной. Далеко не каждая асинхронная функция будет содержать логику, при которой происходит изменение observable-полей. Чтобы подсказывать babel-плагину о необходимости трансформации, в нашем проекте дополнительно сделан кастомный декоратор. Babel-плагин видит его и понимает, что обернутую асинхронную функцию необходимо трансформировать в генератор и дополнительно обернуть результат в mobx.flow-функцию.


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


    Обзор будущего Mobx


    В начале апреля появился пропозал к MobX 6. Эта часть статьи будет посвящена обзору предлагаемых изменений. Основное — декораторы. Создатель библиотеки рассматривает вариант отказа от них, пока декораторы официально не войдут в стандарт джаваскрипта. Такой шаг обусловлен пятью преимуществами:


    • Совместимость с современным стандартом джаваскрипта.
    • Работа библиотеки из коробки.
    • Уменьшение количества вариантов использования Mobx.
    • Уменьшение размера бандла.
    • Прямая совместимость с будущим стандартом декораторов.

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


    Отметим, что у нас в проекте декораторы используются не только в связке с MobX, но и для Dependency Injection. Хоть декораторы и не входят в стандарт джаваскрипта, все-таки они используются во многих библиотеках для простоты и наглядности кода. Ярким примером является Angular и NestJS. Большинство комментаторов в обсуждении к пропозалу пока скептически относятся к удалению декораторов и высказываются за возможность оставить декораторы или вынести их в отдельную библиотеку.


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


    Итог


    Mobx — очень мощный инструмент, который подходит для больших проектов и легок для старта. Но нужно следить за тем, как вы с ним работаете. Используйте computed-значения как можно чаще, делайте компоненты как можно меньше. Старайтесь инжектить данные по месту их использования.

    Tinkoff
    it’s Tinkoff — просто о сложном

    Комментарии 38

      0
      возникают трудности с асинхронностью

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

      Батчит всё по умолчанию и полностью отпадает нужна в action и runInAction.

      codesandbox.io/s/zen-surf-g9r9t?file=/src/App.tsx

      Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций.
        0
        Привет, правильно ли я понимаю, что reactionScheduler просто откладывает вызов реакций после изменения Observable поля? Попробуйте добавить в Mobx конфигурацию строгий режим (настройка enforceActions: «always») и у вас все упадет. Мы же решали проблему асинхронных функций при строгом режиме: изменять Observable поля только из action функций. ReactionScheduler, как мне кажется, не подходит.
          0
          Строгий режим нужен лишь для того, чтобы как раз всегда был батчинг изменений, когда ты изменяешь реактивные переменные, внутри action и runInAction происходит батчинг и enforceActions: «always» проверяет когда ты изменяешь реактивную переменную, включен ли сейчас режим батчинга или нет и если нет, то он падает. А настройка reactionScheduler избавляет от этого, и делает батчинг по умолчанию и настройку enforceActions: «always» не актуальной, ее нужно убрать. Следовательно код становится красивее, его объем меньше и читается он лучше. И при этом ты ни сколько не теряешь в оптимизациях и лишних рендерах.
            0
            А не получится так, что этот шедулер сработает между изменениями observable? И выкатит часть до, а потом обновит оставшуюся?
              0
              Нет, т.к. синхронные изменения состояния происходят синхронно, и уже в свободное время вызывается эта реакция. То есть вы хоть в цикле миллиард раз изменяйте состояние, реакция вызовется только после того, как этот синхронный код отработает. Проверяется тоже легко.
                0
                Не совсем верно. Внутри action любые уведомления об изменении observable откладываются до завершения синхронного кода. Если скажем не использовать action и строгий режим, то уведомления об изменении будут происходить сразу после изменения значения observable свойства.
                  0
                  Вы посмотрите тред комментариев и вот это — codesandbox.io/s/zen-surf-g9r9t?file=/src/App.tsx

                  Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций и перезагрузите страницу и снова посмотрите в консоль.
              0
              И строгий режим не только для батчинга нужен, но и для того чтобы изменять observable поле только из action метода, а не из любого куска приложения, например изменить observable напрямую из компонента нельзя будет, только через action функцию. Помогает не замусорить код и при дебаге видны названия action.
                0
                Если вы не используете Tyescript и пишете код так, что нужно использовать MobX Dev Tools чтобы понять что вообще происходит и почему ХХХ не работает, тогда у меня для вас плохие новости, а в иных случая это не нужно и только лишь засоряет код. Не забывайте, помимо action надо ещё после асинхронных вызовов изменять состояние при строгом режиме через runInAction что так же не удобно и засоряет код.
                изменить observable напрямую из компонента

                А для кого придумали code review?

                Хотите навешивать всюду action и runInAction и принципиально не пользоваться удобством, да ради бога, главное не на моих проектах)
                Ах да, и ещё, игнорировать в IDEшках «Find All References» / «Find Usages» чтобы видеть где читается и изменяется то или иное свойство, где вызывается та или ина функция и т.п. предпочитая этому захламление кода ради того, чтобы потом в один прекрасный момент через dev tools это выяснять… Ну я даже не знаю =) Я серьезно, VSCode — «Find All References», WebStorm — «Find Usages» попробуйте, уверен вам понравится и вы взгляните на «проблемы» отладки совсем под другим углом.
              0
              ReactionScheduler, как мне кажется, не подходит.

              Вот же смотрите, это проверяется на раз два, ещё модицифировал пример конкретно имитирующий асинхронный вызов и после этого модификацию состояния — codesandbox.io/s/staging-bush-52r7q?file=/src/App.tsx

              Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций и перезагрузите страницу.
                0
                membrum MaZaAa
                NB! Вот тут Мишель (автор MobX) отмечает, что он как раз by design стремился к синхронному исполнению реакций, так что откладывание на микротаску (и тем более на макротаску) скорее будет усложнять жизнь.

                и вот ещё из доки:
                reactionScheduler: (f: () => void) => void

                Sets a new function that executes all MobX reactions. By default reactionScheduler just runs the f reaction without any other behavior. This can be useful for basic debugging, or slowing down reactions to visualize application updates.

                mobx.js.org/refguide/api.html#reactionscheduler-f-void-void
                  0
                  Мишель (автор MobX) отмечает, что он как раз by design стремился к синхронному исполнению реакций, так что откладывание на микротаску (и тем более на макротаску) скорее будет усложнять жизнь.

                  Мало ли к чему он стремился, если ему эту усложняет жизнь, это не значит что это усложняет жизнь всем. Вы посмотрите исходники, увидите как он пишет код(кровь может из глаз пойти) и поймете, что не стоит брать его рекомендации как истину. Для UI и для остального не нужны синхронные реакции когда уже пользователь начинает взаимодействие, а вот в момент инициализации нужны, поэтому там и стоит setTimeout чтобы применить reactionScheduler автоматический bulk (асинхронные реакции) именно после синхронной инициализации кода.
                  Вообще это его просчет, что приходится для этого извращаться с reactionScheduler или оборачивать всё и вся в action/runInAction. Вместо того чтобы можно было включать/выключать режим sync / async + auto bulk. Но как обычно, если хочешь чтобы что-то было хорошо, сделай это сам.
                  и вот ещё из доки:

                  Вы пример то смотрели или нет? Там всё явно видно, как именно вызываются реакции, чтобы не оставалось никаких недопониманий. Никаких лишних или неожиданные реакций при настройке reactionScheduler там нет, всё четко и именно так, как нужно.
                  codesandbox.io/s/staging-bush-52r7q?file=/src/App.tsx
                +4

                В вашем примере setTimeout() будет вызван 10 раз, на каждый инкремент!


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

                  –2
                  Вы не понимаете как это работает, при чет тут тормоза и гигабайты памяти? Эта настройка говорит что нужно выполнить реакцию не сразу же после изменения реактивной переменной, а через таймаунт, эти реакции не накапливаются, а выполнятся лишь по разу. Для этого в примере есть console.log в которых всё явно видно. Вместо того что писать всякую ерись, просто посмотрели бы и убедились сразу же.
                +2
                Пример с visible является анти-паттерном, так делать очень не рекомендуется.
                Loader компоненту должно быть всё равно, что там в модели. Его задача — показать индикатор активности. То же самое для AudioComponent.

                Вместо
                function Player({ model }) {
                   return (
                       <div>
                           <Loader visible={model.isLoading} />
                           <AudioComponent visible={!model.hasAudio && !model.hasErrors} />
                       </div>
                   );
                }
                

                Должно быть
                function Player({ model }) {
                   return (
                       <div>
                           {model.isLoading && <Loader />}
                           {!model.hasAudio && !model.hasErrors && <AudioComponent />}
                       </div>
                   );
                }
                


                По поводу useMemo/useCallback. Их нужно использовать в исключительных случаях, например, когда создание объекта/функции является ресурсоёмким. Больше по теме: kentcdodds.com/blog/usememo-and-usecallback
                  0

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


                  А в статье, на которую вы ссылаетесь, useCallback поставлен в неправильном месте. Ну, и ещё используются простые кнопки вместо компонентов.

                    +1
                    Их нужно использовать в исключительных случаях, например, когда создание объекта/функции является ресурсоёмким

                    Не могу не отметить, что это весьма и весьма спорное утверждение. Статью по ссылке читал неоднократно и согласен с ней лишь частично. Устроим holy-war? :)

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

                        В данном случае компонент не получает ссылку на инстанс модели, а получает значение из модели, то есть ничего о существовании модели он по прежнему не знает. Здесь всё нормально и анти-паттерном это не является.


                        UPD: даже если компонент получает инстанс модели через параметр, но в самом компоненте этот инстанс описан интерфейсом, то есть нет импорта из модели, то это по прежнему просто передача значения и тоже всё нормально.

                        –1
                        Button будет перерисовываться при каждом изменении родительского компонента из-за изменения ссылки в параметре onClick. Это нарушает оптимизации приложения и влияет на его производительность.

                        Выглядит как проблема в реализации Button — onClick не должен влиять на результат рендера и соответственно на его вызов.
                          0
                          Привет, на каждый рендер родителя будет создаваться новая функция, которая передается в компонент Button. React сравнит ссылку полученного пропа onClick — ссылка новая, значит компонент перерисуется?
                            –2
                            Да. Это решается через shouldComponentUpdate.
                              0

                              А при нажатии на кнопку какой из onClick должен сработать — переданный первым или свежий?


                              В первом случае рано или поздно у вас всплывут неочевидные баги. И с подходом через хуки это будет скорее рано чем поздно.


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

                                0

                                Свежий onClick мы всегда используем в статическом в рамках компонента handleClick методе, который используется в рендере.

                                  +1

                                  Только этот подход несовместим с хуками.

                          0

                          Спасибо за статью, интересно.
                          Что скажете насчёт mobx-state-tree? Использовали или используете где-то в своих проектах?

                            0
                            привет, не используем на проекте, да и руки как-то не доходили попробовать.
                              0

                              Эхх, только хотел спросить, почему вы не взяли mst, ведь, там решена из коробки проблема асинхронных actions. Легкий доступ к другим частям стора и middleware из коробки.

                                +1
                                В MST как раз предлагают использовать flow и генераторы, вместо async/await: mobx-state-tree.js.org/concepts/async-actions
                                Тоже самое доступно в Mobx. Я не тут не вижу проблемы — код на yield выглядит и читается так же как на async/await.
                            +2

                            Читая статью я совсем не понял, что именно смущает автора в том, что <Player/> ререндерится всякий раз когда меняются поля в модели. Он ведь от них напрямую и зависит. Это явно прописано в его коде. Так оно и должно быть. Никакой проблемы в этом нет.


                            В то же время вынос этих computed полей в VM (раз уж VM есть), дав им человеко-читаемые имена, это нормальное архитектурное решение. Но едва ли это имеет существенное значение в вопросах производительности. Тут скорее разделение компоненты на VM и V. Полагаю в мире MobX это стандартная практика.

                              +1
                              const onClick = useCallback(() => {
                                model.doSomething();
                              }, []);

                              тут надо бы [model]. Возможно model и статичен, но как минимум, глядя на код, это не очень очевидно :)

                                0
                                Спасибо, что поделились своим опытом. Проблема inject в том, что его нельзя типизировать без костылей, примеры костылей описаны в этой статье. С хуками компонент Player может выглядеть так, у него нет проблем с типизацией:
                                function Player() {
                                  const { model } = useStore();
                                  return (
                                    <div>
                                      <Loader visible={model.isLoading} />
                                      <AudioComponent visible={!model.hasAudio && !model.hasErrors} />
                                    </div>
                                  );
                                }
                                
                                export default observer(Player)
                                
                                  0

                                  Inject вроде бы объявлен deprecated. Не нашлось ли у Вас ему адекватной замены для обеспечения строго типизации?

                                    0
                                    Привет, как и рекомендуется на сайте с документацией, используем React Context.
                                  +1
                                  зачем вы используете inject когда рекомендуется использовать react.context?

                                  Warning: It is recommended to use React.createContext instead! It provides generally the same functionality, and Provider / inject is mostly around for legacy reasons
                                    0
                                    Привет, в статье используется inject для примера, мы у себя на проекте реализовали DI и прикидываем модели через React контекст.
                                    +1
                                    Статья популяризует MobX, что не может не радовать, это отличный инструмент, на мой взгляд. Хотя описаны довольно простые сценарии, уверен, этот опыт пригодится разработчикам.

                                    Из недостатков вижу следующее:

                                    1. "@action.bound… Не используйте его, если внутри коллбека нет изменения наблюдаемых полей" — тут сразу ряд вопросов.
                                    — Подразумевает ли это, что @action использовать в подобном случае можно?
                                    — Чем мешает использование @action или @action.bound на методах, не изменяющих параметры стора?
                                    — Чем @action.bound такой особенный?

                                    Вот моя версия ответов.
                                    Если параметры стора данным методом не изменяются — то лучше и не использовать на них эти декораторы (@action или @action.bound), так как они навешивают довольно большое количество логики и будут зазря отъедать процессорное время. Но в большинстве случаев это означает, что данный метод — не часть экосистемы MobX store и может быть вынесен как отдельная функция в утилиты.
                                    Различие между @action.bound и @action минимальное, первый отличается лишь биндингом стора. Смотрим реализацию. Сокращенно:

                                    action = createAction(fn.name, fn)
                                    boundAction = createAction(fn.name, fn.bind(target))
                                    

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

                                    class Store {
                                      constructor() { 
                                        this.myMethod = this.myMethod.bind(this) 
                                      }
                                      
                                      @action myMethod()
                                    }
                                    

                                    Так что ничем @action.bound не особенный, и подходит для всех методов стора в экосистеме MobX, и удобнее эти декораторы навешивать автоматически, если придерживаться паттерна actions-inside-classes. Например, таким декоратором.

                                    2. Конвертация кода в другую структуру (из промисов в генераторы) — сомнительное решение. Вопрос недоверия к преобразованному коду в целом уже не возникает, за последний год я не сталкивался с тем, что babel при трансформации создает нерабочий или не так как нужно работающий код, хотя раньше такие случаи не были редкостью. Но вопрос кроссбраузерности стоит. caniuse.com/#feat=es6-generators говорит, что некоторым проектам с широкой поддержкой браузеров такой вариант не подойдет, так как генераторы используют новые синтаксические конструкции, не поддающиеся полифиллингу.
                                    Я бы однозначно выбрал «придется выносить функцию обратного вызова в отдельный метод», подавив в себе желание «иметь возможность передавать анонимную функцию». Получив анонимный стектрейс… Нет, не замечал за собой такого желания)

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

                                    const AudioComponent = withVisible({
                                      Component: PlayerComponent,
                                      condition: state => !state.hasAudio && !state.hasErrors
                                    });
                                    
                                    function Player({ model }) {
                                      return <AudioComponent conditionState={model} />;
                                    }
                                    
                                    function withVisible({ Component, condition }) {
                                      function Visible({ conditionState, ...otherProps }) {
                                        return !condition(conditionState) ? null : <Component {...otherProps} />;
                                      }
                                    
                                      return observer(Visible);
                                    }
                                    

                                    Тут нужно обратить внимание на то, что так как в этот декоратор передается observable-объект, то нужно добавить observer(Visible) для того, чтобы MobX reaction связал изменения в нем с необходимостью заререндерить этот Visible HOC. Код не проверял, но логических ошибок не вижу)

                                    Другой вариант — передавать <AudioComponent visible="model.showAudio" /> строкой либо годящимся для TS прокси с конвертацией в строку <AudioComponent visible={proxy(model).showAudio.toString()} />, а в декораторе уже подключать соответствующий стор исходя из этой строки и осуществлять MobX observed-наблюдение. Вариант, кстати, годный, но в случае со строками нужна внимательность — при рефакторинге названий параметров подсказок изменить строку не будет, а в случае с линзой получается довольно многословно и нельзя переиспользовать один прокси несколько раз, нужно пересоздавать. Так что недостатки есть, но это в любом случае лучше, чем ререндеры или ручной SCU.

                                    ***

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

                                    const obj = { method() { console.log(this) } }
                                    obj.method();
                                    const fn = obj.method;
                                    fn();
                                    

                                    А в целом — палец вверх за статью!
                                      0
                                      Привет, спасибо за отзыв и найденную оплошность в коде, исправил названия!

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

                                    Самое читаемое