Комментарии 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
Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций.
изменить 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
Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций и перезагрузите страницу.
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 раз, на каждый инкремент!
И после таких вот перлов люди удивляются, что сайты-визитки тормозят даже на современных смартфонах и отъедают по гигабайту памяти.
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? :)
В данном случае компонент не получает ссылку на инстанс модели, а получает значение из модели, то есть ничего о существовании модели он по прежнему не знает. Здесь всё нормально и анти-паттерном это не является.
UPD: даже если компонент получает инстанс модели через параметр, но в самом компоненте этот инстанс описан интерфейсом, то есть нет импорта из модели, то это по прежнему просто передача значения и тоже всё нормально.
Button будет перерисовываться при каждом изменении родительского компонента из-за изменения ссылки в параметре onClick. Это нарушает оптимизации приложения и влияет на его производительность.
Выглядит как проблема в реализации Button — onClick не должен влиять на результат рендера и соответственно на его вызов.
Спасибо за статью, интересно.
Что скажете насчёт mobx-state-tree? Использовали или используете где-то в своих проектах?
Эхх, только хотел спросить, почему вы не взяли mst, ведь, там решена из коробки проблема асинхронных actions. Легкий доступ к другим частям стора и middleware из коробки.
Тоже самое доступно в Mobx. Я не тут не вижу проблемы — код на yield выглядит и читается так же как на async/await.
Читая статью я совсем не понял, что именно смущает автора в том, что <Player/>
ререндерится всякий раз когда меняются поля в модели. Он ведь от них напрямую и зависит. Это явно прописано в его коде. Так оно и должно быть. Никакой проблемы в этом нет.
В то же время вынос этих computed полей в VM (раз уж VM есть), дав им человеко-читаемые имена, это нормальное архитектурное решение. Но едва ли это имеет существенное значение в вопросах производительности. Тут скорее разделение компоненты на VM и V. Полагаю в мире MobX это стандартная практика.
const onClick = useCallback(() => {
model.doSomething();
}, []);
тут надо бы [model]
. Возможно model и статичен, но как минимум, глядя на код, это не очень очевидно :)
function Player() {
const { model } = useStore();
return (
<div>
<Loader visible={model.isLoading} />
<AudioComponent visible={!model.hasAudio && !model.hasErrors} />
</div>
);
}
export default observer(Player)
Warning: It is recommended to use React.createContext instead! It provides generally the same functionality, and Provider / inject is mostly around for legacy reasons
Из недостатков вижу следующее:
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();
А в целом — палец вверх за статью!
Опыт использования MobX в большом приложении