Pull to refresh

Comments 38

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

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

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

Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций и перезагрузите страницу и снова посмотрите в консоль.
И строгий режим не только для батчинга нужен, но и для того чтобы изменять observable поле только из action метода, а не из любого куска приложения, например изменить observable напрямую из компонента нельзя будет, только через action функцию. Помогает не замусорить код и при дебаге видны названия action.
Если вы не используете 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» попробуйте, уверен вам понравится и вы взгляните на «проблемы» отладки совсем под другим углом.
ReactionScheduler, как мне кажется, не подходит.

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

Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций и перезагрузите страницу.
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
Мишель (автор 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

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


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

Вы не понимаете как это работает, при чет тут тормоза и гигабайты памяти? Эта настройка говорит что нужно выполнить реакцию не сразу же после изменения реактивной переменной, а через таймаунт, эти реакции не накапливаются, а выполнятся лишь по разу. Для этого в примере есть console.log в которых всё явно видно. Вместо того что писать всякую ерись, просто посмотрели бы и убедились сразу же.
Пример с 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

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


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

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

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

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

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


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

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

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

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


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


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

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

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

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

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

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

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

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


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

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

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

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

export default observer(Player)

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

Привет, как и рекомендуется на сайте с документацией, используем React Context.
зачем вы используете 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
Привет, в статье используется inject для примера, мы у себя на проекте реализовали DI и прикидываем модели через React контекст.
Статья популяризует 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();

А в целом — палец вверх за статью!
Привет, спасибо за отзыв и найденную оплошность в коде, исправил названия!
Sign up to leave a comment.