Комментарии 242
class PostsStore {
@observable isLoading = false
@observable posts = []
@action getPosts = async () => {
this.isLoading = true
this.posts = await api.getPosts()
this.isLoading = false
}
}
вообще-то этот код не очень корректный, последняя установка isLoading будет вызвана вне контекста выполнения экшена getPosts (mobx все обновления выполняет синхронно), соответсвенно, и перерендер после него не произойдёт.
Подобное поведение отдельно разбирается в разделе документации asynchronous actions
И, пожалуй, такое "неочевидное" поведение тоже можно записать в настоящие минусы mobx-а =)
1) Перерендер произойдет. (https://codesandbox.io/s/determined-browser-zfjyq?file=/src/App.tsx)
2) Вы не поняли походу для чего нужны action/runInAction.
3) Не советую пристально читать то, что пишут в документации к MobX, к сожалению там имеется ересь.
P.S. вообще от action и runInAction можно легко отказаться и включить автоматический батчинг
import { configure } from 'mobx';
setTimeout(() => {
configure({
reactionScheduler: (f) => {
setTimeout(f, 1);
},
});
}, 1);
Перерендер произойдет. (https://codesandbox.io/s/determined-browser-zfjyq?file=/src/App.tsx)
хм, Ваша правда… интересно, почему..
Вы не поняли походу для чего нужны action/runInAction.
Возможно… я опирался на вот эти строки в документации:
By default, it is not allowed to change the state outside of actions. This helps to clearly identify in your code base where the state updates happen.
Вынужден поверить в это:
Не советую пристально читать то, что пишут в документации к MobX, к сожалению там имеется ересь.
А что ещё там есть из ереси?
вообще от action и runInAction можно легко отказаться и включить автоматический батчинг
И когда он будет запускаться? Каждый раз при изменении observable / computed?
О, я даже уже делал это codesandbox.io/s/zen-surf-g9r9t там надо смотреть в консоль и комментировать/разкоментировать конфиг mobx'a чтобы смотреть на результат.
Основной поинт такой:
1) При инициализации все работает штатно и все реакции синхронные, это обязательно нужно т.к. в момент инита важна синхронность, лень расписывать реальные примеры из жизни, можете просто поверить на слово) Ну либо однажды в этом убедиться лично)
2) После того, как весь синхронный код отработает, как раз через setTimeout будет изменен шедулер реакций, который будет откладывать выполнение реакций через setTimeout, то есть у вас синхронно что-то меняется, но реакции сразу же не вызываются, они будут запланированы через setTimeout после того, как все ваши синхронные изменения закончатся. Как раз это то, что нужно web приложению, чтобы не делать лишних рендеров и реакций, а подождать пока батч синхронных изменений закончится и уже после этого вызывать реакции на эти изменения.
Короче, подписывание ВСЕГО вашего кода под асинхронное выполнение реакций выглядит хорошим только тогда, когда вы реально весь код контролируете, и считаете, что да, вам так делать норм. В больших проектах под много людей такое делать — ну чёт совсем не очень.
А мне идея с setTimeout(f, 1) (вообще-то тогда уж 0, если на то пошло) совсем даже не кажется хорошей. То, что в коде написано синхронно (некая цепочка реакций), и раньше бы могло отработать синхронно (и главное — могло бы оптимизироваться компилятором) — теперь у вас на каждый шаг по цепочке будет бахать новый таск в event loop, ну и JIT в такое не умеет тоже, к слову.
При инициализации все работает синхронно, после инициализации асинхронно(авто батчинг), в 99.9% случаев в ходе работы приложения синхронные реакции не нужны.
Короче, подписывание ВСЕГО вашего кода под асинхронное выполнение реакций выглядит хорошим только тогда, когда вы реально весь код контролируете, и считаете, что да, вам так делать норм. В больших проектах под много людей такое делать — ну чёт совсем не очень.
В самом большом моем проекте из последних, с командой из 10+ человек вообще проблем НОЛЬ. Только удовольствие от максимально чистого и минимального кода. И от того, что монструозный проект не тормозит (общая производительность зависит только уже от быстродействия АПИ).
Так что религиозные предубеждения и забота о JIT компиляторе вам только вставляют палки в колеса.
Заставить фронтенд проект тормозить по вине MobX'a ну это надо серьезно постараться. Хотя если вы используете mobx-state-tree, тогда вы можете легко заставить свой крупный проект тормозить из-за гипер излишней работы в ран тайме.
При инициализации все работает синхронно, после инициализации асинхронно(авто батчинг), в 99.9% случаев в ходе работы приложения синхронные реакции не нужны.
Ну пардон, наличие или отсутствие цепочек синхронных реакций (а так же их длина) — зависят от архитектуры проекта, а не то, что это какие-то редчайшие ситуации. Они редчайшие, если у вас фронт тупой, и, как и множество типичных фронтов — просто что-то там загружает и показывает и немного потом интерактивит в духе onclick -> запульнуть жсон на бэк. Но далеко не все фронты такие. Когда на фронте много логики — цепочки реакций у вас скорее всего будут достаточно длинные, не говоря уж про то, что еще и динамически изменяющиеся в рантайме.
Заставить фронтенд проект тормозить по вине MobX'a ну это надо серьезно постараться.
Да ладно, я заставлял, и даже без особых усилий. Всего лишь отсутствие throttle в нужных местах в некоторых спамящихся мутациях (а-ля позиция скролла) способно поставить любой стейт-менеджмент на колени. Там и vanillajs даже еле справляется, если написать обработку тупенько, в лоб, и так, чтоб JIT-компайлер её не смог заоптимизировать.
Они редчайшие, если у вас фронт тупой, и, как и множество типичных фронтов
Когда на фронте много логики — цепочки реакций у вас скорее всего будут достаточно длинные, не говоря уж про то, что еще и динамически изменяющиеся в рантайме.
Так и в чем проблема-то, если реакции будут не синхронны, а забатчены автоматом?? Вы просто возьмите и засуньте этот конфиг в ваш мега сложный и крутой проект и посмотрите, сломается ли он или нет.
Вместо философских рассуждений и боязни того, что не произошло и вообще не факт что произойдет, можно проверить это легко и быстро на практике.
Всего лишь отсутствие throttle в нужных местах в некоторых спамящихся мутациях (а-ля позиция скролла) способно поставить любой стейт-менеджмент на колени.
$mol_atom не поставит. Он автоматически троттлинг реакции до следующего фрейма.
А мне идея с setTimeout(f, 1) (вообще-то тогда уж 0, если на то пошло) совсем даже не кажется хорошей. То, что в коде написано синхронно (некая цепочка реакций), и раньше бы могло отработать синхронно (и главное — могло бы оптимизироваться компилятором) — теперь у вас на каждый шаг по цепочке будет бахать новый таск в event loop, ну и JIT в такое не умеет тоже, к слову.
Реакции в любом случае "бахаются" в reaction loop, что точно так же не может быть оптимизировано компилятором. Не вижу что тут меняет асинхронный запуск reaction loop по отношению в мутации.
P.S. вообще от action и runInAction можно легко отказаться и включить автоматический батчинг
Вы забыли добавить антидребезг. При множественных мутациях состояния у вас будет создано слишком много таймеров.
Вы забыли добавить антидребезг. При множественных мутациях состояния у вас будет создано слишком много таймеров.
Ничего страшного, стэк не переполнится, каких-то фризов вы не увидите. А если у вас переполнится стэк, то с вашим приложением проблемы)
Разумеется, он не переполнится. Но почему это повод делать лишние действия? Добавление таймера, пусть даже и "пустого", всё-таки не самая дешевая операция, зачем делать её лишний раз?
— Мы оперируем гигабайтами памяти, даже на мобильных устройствах. Не мега, а гига.
Зная это, мы говорим о том, что пустой таймаут это не самая дешевая операция? При этом мы использует монструозные фреймворки и библиотеки…
Разумеется, он не переполнится. Но почему это повод делать лишние действия? Добавление таймера, пусть даже и «пустого», всё-таки не самая дешевая операция, зачем делать её лишний раз?
Лишние действия — в замен на более чистый код, а более чистый код в этом случае побеждает. Всё просто.
а более чистый код в этом случае побеждает
Каким образом?
@action
fetch = async () => {
this.isFetching = true;
try {
const response = await getApiData();
runInAction(() => {
this.items = response.items;
this.totalCount = response.totalCount;
});
} catch (e) {
runInAction(() => {
this.items = [];
this.totalCount = 0;
this.error = e.message;
});
} finally {
runInAction(() => {
this.isFetching = false;
this.error = null;
})
}
}
vs
fetch = async () => {
this.isFetching = true;
try {
const response = await getApiData();
this.items = response.items;
this.totalCount = response.totalCount;
} catch (e) {
this.items = [];
this.totalCount = 0;
this.error = e.message;
} finally {
this.isFetching = false;
this.error = null;
}
}
Мы избавились от 1 action и от 3х runInAction
Почему не использовать flow
и синтакс генераторов?
const test = yield 1; // у переменной test тип any вместо number
Не то, чтобы не дружит, просто генераторы не позволяют выводить тут тип, ибо yield действительно может вернуть всё что угодно в общем случае. Поэтому надо указывать ожидаемый тип явно:
Сейчас с TS можно спокойно подружить yield*, и если в принципе переход от yield X к yield* GENWRAP(X) не вызывает жжения в пятой точке, то можно действовать примерно вот так.
Мы избавились от 1 action и от 3х runInAction
Зато получили 8 setTimeout.
Зато получили 8 setTimeout.
Какой кошмар, от этого приложение перестало работать или стало работать медленнее?
Откуда тут 8 таймаутов?
На каждое изменение любого observable вызывается reactionScheduler, который делает setTimeout.
Они же автобатчатся в один вызов шедулера.
Кем? Я не вижу этого в коде.
Это ж индивидуальный флаг для каждой реакции. И он проверяется уже в цикле обработки реакций, т.е. внутри функции, передаваемой в reactionScheduler. На вызов reactionScheduler он никак не влияет.
Это локальная переменная авторана. Вот если будет несколько авторанов, то да, будет несколько таймаутов. В $mol_atom же будет один единственный requestAnimationFrame в любом случае.
Какая разница сколько там авторанов? reactionScheduler вызывается совсем в другом месте независимо от их количества.
Минимальная частота моего процессора — 400 мегагерц. В ней он очень холодный и ест очень мало энерии. Если ваш сайт заставляет мой девайс греться и есть батарею, то возникает желание закрыть этот сайт поскорее.
По кайфу писать громоздкий код и жертвовать многим в угоду того, чтобы у дяди Пети на 1mAh меньше батарейки съел сеанс работы с приложением, пожалуйста я не против.
Только вот не надо всех остальных под эту гребенку загонять и думать что это реально имеет значение и оказывает влияние на деньги, которые целевая аудитория приносит бизнесу.
А вот поддержка «такого вот» кода РЕАЛЬНО обходится намного намного дороже и дольше по времени, для бизнеса который тебе платит, чем забота о потреблении на 1mAh меньше. Более такого на «такой вот» код ещё и надо кого-то найти, кто согласится с ним работать.
На дворе почти 2021 год, а мы не микроконтроллеры программируем с тактовой частотой 32kHz и RAM в 4kb. Вот там РЕАЛЬНО надо экономить на тактах процессора и экономии в несколько байт памяти, потому что на этом уровне это действительно заметно.
Строго говоря добавление таймера — дешёвая операция, хоть она и Oмега(n) (всегда столько времени, сколько таймеров уже добавлено) на вставку и создание контекстов на чтение/исполнение, всё равно даже 1000 таймеров не сравнится с рендером всего одного (!) абзаца текста с кастомным шрифтом. Так что антидребезг был бы хорош, но точно не стоит такого напора)
Тем не менее, оптимизировать нужно там где бутылочное горлышко, а не там где это интуитивно кажется по фрагменту кода, опубликованного тут в комментариях
Вы же, когда код пишете, не говорите «ну вот я тут заведу массив неиспользуемых значений на мегабайтик, всё равно сейчас памяти у всех гигабайты, и тут не будет бутылочного горлышка»?
Не очень понимаю о чём весь этот тред\срач. Мне кажется ситуация простая:
- Имеем observable модель. Стало быть при изменении нужно notify всех subscribers.
- Можно это сделать immediately, и тогда привет либо а) костыли вроде runInAction; б) ацкие тормоза. Либо отложенно
- Отложенная модель прекрасна. Но её можно имплементировать по-разному.
- Можно сделать 1 setTimeout/requestAnimationFrame/nextTick/whatever и очередь обновлений (одну на всех). ЕМНИП то deferred observable в KnockoutJS так и работают. Судя по всему так же делает и $mol.
- Но если сделать таймауты на каждый change, то получается полная ерунда.
Мне кажется весь этот срач можно завершить просто проверив как делает MobX, как в п4 или как в п5.
Или я не прав?
P.S. В KnockoutJS они намудрили и выдали возможность сделать как угодно. Там у каждого observable может быть своя модель обновлений.
Не очень понимаю о чём весь этот тред\срач.
Попробуйте прочитать его еще раз, медленнее.
Можно это сделать immediately, и тогда привет либо а) костыли вроде runInAction; б) ацкие тормоза. Либо отложенно
Простите за резкость, но это бред. Синхронное оповещение подписчиков (ваше immediately) не порождает никаких костылей (серьезно, вам не нужен runInAction, он даже и при асинхронных оповещениях далеко не всегда нужен), ни тем более тормозов (если вы конечно не будете синхронно делать такой громадный объем вычислений, который таки тормоза даст).
Речь в этом треде как раз идёт о том, что предложенный «элегантный выход» делает любые оповещения отложенными, без всякого разбора. Даже если они вообще-то исполняют только синхронный код, и могли бы без этого финта ушами выполняться синхронно. И всё для того, чтоб оповещения при выполнении асинхронного кода можно было бы записать чуток короче (но нет, потому что с применением flow тоже было бы чуток короче).
Синхронное оповещение не порождает… ни тем более тормозов
WAT? В смысле не порождает? Да тот же пример выше явный пример того, что порождает. Вот возьмём связку React + MobX (либо Knockout в базовом виде).
Имеем:
this.a = 1; // rerender 1
this.b = 2; // rerender 2
this.c = 3; // rerender 3
Получается 3 рендера компонента. Вместо 1-го. Первые 2 не нужны были. Это ведь множество аллокаций и довольно тяжёлые реконсиляции. Вы же не будете утверждать, что 1 запись в event loop сопоставима со 2-мя лишними рендерами?
Или я тут что-то недопонимаю?
Простите за резкость
Всё же давайте без резкости. Я понимаю, русскоязычное комьюнити и всё такое. Но куда плодотворнее дело пойдёт, если приглушить эмоции и оперировать фактами\доводами.
вы конечно не будете синхронно делать такой громадный объем вычислений, который таки тормоза даст
Разумеется буду. Мы же пишем UI приложение. Ну пусть не "громадный", но так или иначе куда более тяжёлый, чем 1 setImmediate + [].push
.
Даже если они вообще-то исполняют только синхронный код, и могли бы без этого финта ушами выполняться синхронно. И всё для того, чтоб оповещения при выполнении асинхронного кода можно было бы записать чуток короче
Я конечно могу ошибаться, но, мне кажется, синхронные уведомления это такая бомба замедленного действия, которую можно применять только тогда, когда отлично понимаешь, чего это будет стоить и подложишь где нужно соломинку. Т.е. я как раз поддерживаю асинхронные уведомления как дефолт. И не возражаю против возможности где надо — использовать синхронный вариант.
ИЧСХ то же самое сделали авторы спецификации к Promise.
Да тот же пример выше явный пример того, что порождает.
Вы же понимаете, что вы сейчас пытаетесь поговорить про две разные системы? Синхронное выполнение реакций ну никак не мешает асинхронно батчить изменения перед рендером (что, собственно, в mobx-react-lite и произойдет, вот только клей mobx и react — это не одно и то же, что и сам mobx).
Разумеется буду.
Делайте. Там, где это вам нужно — делайте асинхронно, а не везде вообще, просто потому что. Для этого, в конце концов, надо только написать async и еще пару слов, и сразу всё будет шоколадно.
Я конечно могу ошибаться, но, мне кажется, синхронные уведомления это такая бомба замедленного действия
А мне кажется, что спам асинхронных тасков в event loop по любому поводу — это гораздо более интересная бомба замедленного действия, которая в простых случаях не рванёт и всё будет норм — но вот зато когда рванёт, то выживших не будет вообще.
А мне кажется, что спам асинхронных тасков в event loop по любому поводу — это гораздо более интересная бомба замедленного действия, которая в простых случаях не рванёт и всё будет норм — но вот зато когда рванёт, то выживших не будет вообще.
Вам так только кажется, если она рванет, то только потому что вы сделали что-то неправильно, в асинхронном мере JS, все должно быть асинхронно. И код должен писаться исходя из того, что мы находимся в асинхронной среде, а не в синхронной, где всё выполняется строго сверху вниз.
Синхронное выполнение реакций ну никак не мешает асинхронно батчить изменения перед рендером
А можно с этого момента поподробнее? Вот это уже интересно и конструктивно. Полагаю, нечто подобное, должно быть во Vue. А как это реализуется в случае MobX? И реализовано ли оно так? Если да, то зачем вообще нужен runInAction?
спам асинхронных тасков в event loop это гораздо более интересная бомба замедленного действия
Тоже интересно. Предложите такой сценарий когда это может оказаться бомбой. Ну т.е. приведёт к неожиданному провалу по производительности. Сразу уточню — я имею ввиду вариант под п4, когда изменения batch-ятся. А не когда на любой чих вешается свой таймаут.
К примеру никакой бомбы за всё время использования мною promise-ов я не заметил. Был только 1 tricky case когда асинхронная природа вычисления sha1 браузерным стандартным API поставила крест на его использовании вообще и я взял синхронную JS реализацию. Но это прямо особенный случай.
Имхо, вся фишка mobx как раз в том, что уведомления синхронно происходят. Если хочется по дефолту асинхронно, то лучше тогда уже использовать rxjs
Рвать волосы и пытаться доказать что эти таймауты приведут к отжиранию ресурсов — смешно, потому что это не так, ни вы, ни кто-то другой этого не заметит никогда. Да, и ваш телефон в том числе. Ресурсы отжирают другие вещи например сам браузер, его движок и многое другое, а несколько таймаутов это просто капля в океане, ну реально смешно.
Просто сам факт использования реакта + зоопарка = мега не эффективное использование вычислительных ресурсов, но вас это не волнует, вас волнует парочка таймаутов.
Вас не волнует когда оперируя иммутбильностью вы создаете целые копии объектов выделяя кучу памяти для их хранения и заставляете GC потеть постоянно, более того выделение памяти это вообще не бесплатно для процессора, это очень накладная операция, но это ни кого не волнует, ведь тут парочка таймаутов, которая убивает производительность.
Смешно. «Борцы» за производительность.
Вас не волнует когда оперируя иммутбильностью вы создаете целые копии объектов выделяя кучу памяти для их хранения и заставляете GC потеть постоянно, более того выделение памяти это вообще не бесплатно для процессора, это очень накладная операция, но это ни кого не волнует, ведь тут парочка таймаутов, которая убивает производительность.
Никогда не думал что я плюсану хоть 1 твой комментарий, но вот тут ты прав. Складывается ощущение, что JS-народ очень избирательно смотрит за производительностью.
К примеру аргумент против "иммутабельность в JS тормозит, т.к. нет иммутабельных структур данных" обычно приводят такой: "песочница в gc очень быстрая и оптимизирована под коротко живущие объекты". Хотя какой бы быстрой она не было это всё равно прорва операций аллокации и много работы для gc. Хоть как пыжся, но это приличный объём работы
Или когда ругаются против мемоизации — спорят про то что shallow-реконсиляция не бесплатна и все эти проверки отъедают CPU. Но забывают что даже 1 лишний render среднего размера компонента это ну просто в РАЗЫ большее количество работы для CPU.
Или спорят про ++i vs i++, в то время как даже два запроса параллельно выполнить забывают.
И т.д. и т.д. Один товарищ тут на хабре даже долго и упорно втирал что не использует AJAX запросы, т.к. они медленные и вместо этого у него всё на WebSocket-ах без JSON-а. А HTML на стороне бакенда он генерирует руками написанными StringBuilder.append-ми (адский код из кошмаров).
Или этот тред. Где 1 таймаут противопоставляется лишнему рендеру и таймаут рассматривается как более тяжёлая операция (WAT?). Наверное потому что "реконсиляция быстрая" :)
Складывается ощущение, что JS-народ очень избирательно смотрит за производительностью.
Тут ничего не складывается, так оно и есть, это не только касаемо JS, это касаемо целой индустрии. В итоге всё это приводит к абсурдным разговорам и спорам, как комментарии в этом треде от «борцов за производительность».
Вы уже несколько комментариев подряд пытаетесь поставить себя по другую сторону баррикад, хотя вам довольно давно сказали про flow, и про настолько же «чистый» код, как и в ваших примерах. Так что никакой дихотомии «или-или» тут просто нет, просто вы с помощью «хитрого приёмчика» делаете работу, которой можно было и не делать, и всё так же иметь «чистый» код.
1) Мне не нравится заворачивать функцию в другую функцию flow и использовать синтаксис генераторов. Я предпочитаю async/await.
2) Тут дело не только в асинхронных функциях, вы вообще где либо можете менять стейт и у вас будут лишние рендеры и реакции, чтобы их не было, надо все заворачивать в action/runInAction и тут flow и генераторы не помогут.
Вопрос зачем? Если можно этого не делать и ничем при этом жертвовать не придется, я надеюсь мы уяснили что несколько таймаутов вообще ни как не связаны с ресурсами и производительностью.
Если раскидать операции браузера в виде чего-то вроде этой диаграммы, то таймауты будут на одной стороне спектра (очень дешевые), а на другой стороне будет что-то вроде repaint операции.
Таким образом, миллион созданных таймаутов не перевесят даже один лишний рендер. Вот их и нужно отслеживать и оптимизировать, а не докапываться к таймаутам.
Я не говорю, что вариант с setTimeout самый идеальный (можно заменить его на microtask или вообще убрать, как предлагает JustDont). Но вот обвинять setTimeout в проблемах производительности точно не стоит.
Поделюсь своей трустори на эту тему..
В $mol_view для виртуализации необходимо отслеживать визуальное положение элемента в реальном времени. Единственный надёжный способ это сделать — дёргать getBoundingClinetRect в requestAnimationFrame. Когда такой элемент только один, то 60 раз в секунду дёргать всё это не накладно — на моей машине это где-то пол миллисекунды или три процента нагрузки на проц, когда открыта вкладка. Но когда отслеживаемых элементов становится десятки, то без группировки в один requestAnimationFrame нагрузка на проц становится уже такой, что кулер начинает подавать голос. А с группировкой всё норм, укладываемся в 1 миллисекунду.
Таким образом, миллион созданных таймаутов не перевесят даже один лишний рендер
Это вы погорячились. 1кк таймаутов это прямо дофига. А если под рендером подразумевать рендер virtual-dom, то тем более.
На самом деле асинхронщина не такая быстрая. К примеру у меня была задача выполнять сотни тысяч sha1 операций над короткими строками. Для этого можно воспользоваться браузерным api crypto. Но тут засада. Он возвращает promise. Итог: использовать для этого дела C++-ый crypto оказалось значительно медленнее чем взять синхронную версию на JS с использованием asm.js. Разница была — небо и земля. Версия на WASM, впрочем сильно на фоне JS + asm.js не выделялась.
Так что все эти наши setTimeout, setImmediate, process.nextTick, requestAnimationFrame и пр. далеко не такие быстрые операции.
Однако соглашусь с тем, что едва ли 1 дополнительный setTimeout можно сравнивать с лишним vdom-рендером хотя бы средних размеров react компонента.
Возвращаясь к вашему пример с sha1: синхронный код разумеется будет работать быстрее, не в плане оптимально по процессорной нагрузке, а том плане чтобы он захватит на себя ресурсы системы. В отличии от асинхронных вызовов которые дают поработать остальной системе.
P.S.
Затраченное время не равно процессорное время.
Нагрузка измеряется именно процессорным временем, а не просто временем выполнения кода.
Не стоит об этом забывать.
Честно говоря, я не силён в переключениях контекста и прочем системном программировании, но ЕМНИП то у Promise своя очередь в event loop c очень высоким приоритетом. И я думаю что, тут вся загвоздка именно в накладных расходах на создание и обработку промисов, нежели переключение контекста на уровне ОС и CPU.
Насколько я понимаю, ситуация когда одно приложение могло узурпировать целое ядро процессора и не отпускать его до первого прерывания, это что-то родом из 90-х и сейчас это работает более сложным образом. Тут я думаю 0xd34df00d может подсказать.
while(true){}
повешает свой поток. Это я в курсе. Но как я могу "легко проверить" повешал ли этот код ядро CPU? Вы ведь вроде об этом выше писали.
Разве что запустить Х worker-ов, где Х = числу ядер CPU. Согласно вашей логике выше это должно намертво повешать всю систему. Опыт показывает, что это не так.
Или я не правильно понял ваш message?
Суть в том, что скорее всего, event loop в lubuv и работает как раз синхронно всё то время, пока его очереди не пусты. Но тут я уже лезу далеко за пределы своих познаний.
Короче мораль простая, не надо строить иллюзий и летать в облаках думаю что event loop тормозной, единицы и даже десятки setTimeout'ов убивают производительность и т.д. и т.п.
Это приводит к ложному понимаю картины мира, к говнокоду, к пустым и нелепым спорам и много к чему другому.
Возвращаясь к вашему пример с sha1: синхронный код разумеется будет работать быстрее, не в плане оптимально по процессорной нагрузке, а том плане чтобы он захватит на себя ресурсы системы. В отличии от асинхронных вызовов которые дают поработать остальной системе.
Этого как раз довольно просто избежать при проверке. Достаточно лишь не нагружать ничем остальную систему.
Тезис гигабайт и гигагерц о том, что давным давно уже в нашем распоряжении большие мощности, поэтому не стоит доводить коммерческую разработку до абсурда и заботится о каждом тике процессора и о каждом байте выделенной памяти.
Неплохой ноут работает с 8Гб и 1.8ГГц. На всю систему. Поэтому если одно окно Хрома сожрёт гигабайт и гигагерц — всей остальной системе придётся подвинуться и потупить. А если в фоне уже висит такое окно, то ваше с такими требованиями я могу и вообще не дождаться. Например, Фейсбуком я себя не могу заставить пользоваться, хотя по работе надо. Раз в неделю захожу, и то бесит именно скоростью.
Да, монструозные фреймворки и библиотеки тоже жрут много, но это не повод считать, что так и надо, и рассчитыввать, что гигагерцы и гигабайты полностью ваши.
const obj = {};
for (let i = 0; i < 100; i++) {
obj["prop" + i] = "value" + i;
}
let result = [];
const begin = new Date().getTime();
for (let i = 0; i < 100; i++) {
result.push(JSON.stringify(obj));
}
const end = new Date().getTime();
console.log(`took ${end - begin}ms`);
Посмотрите на этот код и на то, сколько он выполняется по времени. Потом сравните с несколькими безобидными setTimeout'ами в качестве расплаты за более чистый код и подумайте ещё раз, действительно ли оно стоит того или нет?
Да, монструозные фреймворки и библиотеки тоже жрут много, но это не повод считать, что так и надо, и рассчитыввать, что гигагерцы и гигабайты полностью ваши.
Тоже жрут много? Они всё и жрут, как же смешено слушать тех, кто пишет вэб приложения используя фреймворки и библиотеки, всё это работает в браузерах которые отнимают тонны оперативной памяти и процессорного времени на рендеринг и на работу. А потом рассуждают о том, как бы сэкономить пару килобайт памяти и пару наносекунд процессорного времени, но при этом разумеется за счет ухудшения качества кода. Это как зачерпнуть кружкой из океана и думать о том, что ты его наполовину осушил.
Я вам по секрету скажу, что ваше приложение не единственное на компьютере пользователя. А если не соблюдать чистоплотность, то легко засрать любые объёмы ресурсов.
Очень плохо, что вы ради своей супер-странички пытаетесь оперировать на моем телефоне гигабайтами памяти. Скажите адреса проектов, которые вы поддерживаете, внесу их в черный список.
вообще я счас работаю с redux-проектом и mobx-проектом и разница колоссальна. Не больше, не меньше. В голом Redux 10 раз думаешь, прежде чем создать новый экшн, свойство, там прям изгаляешься, в MobX пишешь стор, экшены, свойства без боли, регистрации и и СМС.
И даже сейчас смотрю на Effector и из коментов взял на вооружение $mol_atom
1) сделать 100% кода «окрашенным» (привет redux-saga и прочие решения на генераторах) — это прекрасно, вот только быстродействие страшно просаживает, потому что async-код очень плохо оптимизируется JIT-компилятором;
2) заставлять программиста руками что-то писать в каждом случае выхода из «окрашенного» кода. Это вот как раз эти runInAction от mobx.
Строго говоря никто не мешает писать вызов экшена метода после возврата асинхронной функции. Просто теряется кусок магии лаконичности, но иногда рождается магия низкосвязности
3) Использовать $mol_fiber или аналоги, где не нужно ничего "окрашивать" в месте вызова.
Публичные я собираю тут: https://showcase.hyoo.ru/
Хм, да и posts по идее не должен установиться (ну т.е. он конечно запишется, но PostsPage об этом не узнает)… скажите, а Вы вообще запускали этот код? он правда работает?
Контекст тут ни при чём, перерендер происходит всегда и при любом обновлении.
Вот что правда может произойти в подобном коде — так это лишние рендеры. Но в этом коде лишних присваиваний нет, так что лишних рендеров тоже не будет.
Чтобы запретить мутировать состояние вне хранилища, можно использовать флаг enforceActions в настройках MobX. Но он будет предупреждать вас только в рантайме и создавать проблемы с Promise.
Второй вариант — помечать поля объекта как private. Но в этом случае на каждое приватное поле вам придется создать геттер.
Я бы сказал, что ОБА этих варианта следует не просто применять, а даже и насаждать в коде. Почему? Потому что они ценой некоторой большей многословности (но даже рядом не стоящей с многословностью редакса) делают явными запуск мутаций и обращения к модели. Вообще, заворачивать поля модели в геттеры, а не просто выставлять их наружу — хороший тон не только для mobx, но и вообще для кода.
Вообще, заворачивать поля модели в геттеры, а не просто выставлять их наружу — хороший тон не только для mobx, но и вообще для кода.
Даже когда они просто используются при рендеринге?
Мне кажется что redux решил проблему "многословности" с помощью redux toolkit. Очень приятно было с ним поработать в первый раз (до этого я использовал исключительно mobx).
Я думаю что минимальный вариант будет выглядеть как-то так:
import { useDispatch, useSelector } from 'react-redux';
import { increaseCount, decreaseCount } from './store';
const App = () => {
const count = useSelector(state => state.counter);
const dispatch = useDispatch();
const increase = () => dispatch(increaseCount);
const decrease = () => dispatch(decreaseCount);
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>increment</button>
<button onClick={decrease}>decrement</button>
</div>
)
}
Store:
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: counter,
0,
reducers: {
increaseCount(state) {
state += 1;
},
decreaseCount(state) {
state -= 1;
}
}
});
export const { increaseCount, decreaseCount } = counterSlice.actions;
Уже сильно лучше, того, что было изначально. Но очень забавно наблюдать как ребята продают свои идеалы
reducers: {
increaseCount(state) {
state += 1;
},
decreaseCount(state) {
state -= 1;
}
}
Что мы имеем:
- reducer-ы и action-ы завязаны друг на друга 1 к 1
- нет отдельных action-type-ов
Redux-фанат из прошлого пришёл бы от этого в ярость. Где же reducer-ы с O(n)
и waterfall
? Где же PubSub
говно-архитектура? Где же возможность вызывать один case-reducer-а разными action-ами? Где же столь любый перечень доступных констант action-ов. Ну и прочий бесполезный мусор, который считался жизненно необходимым любому крупному приложению :-)
const count = useSelector(state => state.counter);
А как же умные и глупые компоненты? А где же селекторы и всякие линзы? Вы же в store лезете руками прямо в… Кровоизлияние в мозг. Или… Хм... Оказывается можно и без двадцати уровней абстракций поверх абстракций? Redux фанаты прошлого уже зовут санитаров.
const increase = () => dispatch(increaseCount);
const decrease = () => dispatch(decreaseCount);
Тут честно говоря грязновато получилось. Вполне могли бы сделать так:
<button onClick={dispatch.factory(increaseCount)}/>
со встроенной мемоизацией. Или что-нибудь вроде этого:
const { increaseCount } = useReduxStore().actions;
Было бы что-то вроде Vuex.
Правда ещё вопрос как эти слайсы в друг друга подключаются. Может быть если построить из них дерево...
reducer-ы и action-ы завязаны друг на друга 1 к 1
На самом деле это всё-таки дефолтное поведение. Разработчики редакса пришли к выводу, что реакция в разных reducer'ах на один и тот же action нужна обычно для 3.5 мест и далеко не в каждом проекте и запихнули его в extraReducers.
нет отдельных action-type-ов
Они генерятся на основе имени action'ов и слайсов и пихаются поле name соответствующего actionCreator'a. Иначе бы у людей саги не работали.
нужна обычно для 3.5 мест
Что многим было очевидно с самого начала :)
Они генерятся на основе имени
Ага, я понимаю. Каждый кто писал свои велосипеды поверх redux так или иначе решали эту проблему. Я в одной из попыток даже плагин для babel-я делал. Хотя самое простое решение как раз выше — слить воедино reducer и его action.
Я думаю, что каждый уважающий себя фронтендер должен написать обертку над Redux для борьбы с бойлерплейтом.И я тоже написал решение для борьбы с бойлерплейтом. С помощью кодогенерации с функции вида smth(prev: MyChildState, { params }): MyChildState {} генерируется класс smthAction, у которого поля совпадают с params, редюсер (добавляется case в switch), и диспетчер, чтобы не создавать action, а вызвать метод с теми же параметрами.
Также решена и проблема O(n), теперь это O(1).
Но работает только с typescript.
Ну и хотелось бы писать в мутабельном стиле, поэтомуа мне хочется в иммутабельном писать. Пописав какое то время чистый код, приходишь в проект где «обычный порошок» и рвешь волосы на голове, когда натыкаешься на всякое такое, например почему if не сработал, хотя объекте нужное поле есть, очевидно поля не было в момент прохождения кода, кто то его мутировал позже, а консоль делает evaluation, когда мышкой тыкаешь, а не когда log вызвался.
К хорошему быстро привыкаешь, и именно переход обратно к плохому сильно заметен.
хотя объекте нужное поле есть, очевидно поля не было в момент прохождения кода, кто то его мутировал позже, а консоль делает evaluation, когда мышкой тыкаешь, а не когда log вызвался
При чём тут мутабильность если у вас такой «хороший» код?))) Проблема легко решается сменой работы желательно на проект с нуля.
К хорошему быстро привыкаешь, и именно переход обратно к плохому сильно заметен.
Смешно. Хорошее и плохое. Причем тут «A» хорошо, а «B» плохо. Говорите как есть, что вы не умеете «B», поэтому «A» лично вам больше подходит.
Человек говорит не про то, что он не умеет, а что коллеги не смогли или сделали непривычно для него (кто-то способен держать в голове 5 элементов или даже 6, но многие не больше трёх и это, обычно, потолок), так что я понимаю автора. Хоть и люблю МобХ за его возможную лаконичность, как в TS, так и в CS.
А вообще в чем реальная проблема отладки? Вот прям на полном серьезе.
Если вам не хватает пары тройки console.log и в IDE «Find Usages» / «Find All References», то боюсь такого типа проект не подлежит дальнейшей поддержки и развитию и это кандидат номер 1 на переписывание с нуля и по человечески.
Редьюсер — это просто чистая фукция-сеттер. Причем реализована довольно костыльно — на switch-ах с передачей фактически имени функции (action), которую нужно выполнить. Другими словами, вместо нескольких функций, все запихано в большую функцию со switch.
Селектор — заменяется обычным геттером.
Вот как на mobx может выглядеть аналог функционала redux-ых редьюсеров и селекторов. Как видите, сайд эффекты легко можно вынести из стора. А redux-овы dispatch, actions, action creators заменяются простым вызовом функции.
Даже в Vue, похожем на React, прекрасно обходятся без redux
Хехе. Хехе. Они придумали Vuex. А другие ребята придумали MobX-State-Tree. А сам Redux появился от Flux. А оный ещё от кучи решений до него. Однонаправленная шина данных не бог весть какое изобретение. Я думаю redux просто стал первым распиаренным решением в мире JS. Глобальный стор — тоже довольно очевидная штука. Тут больше вопросы возникают к Абрамову — на кой чёрт столько бойлерплейта на ровном месте? :) Но, кажется, ему просто не нравится DRY
MobX-State-Tree хорош! почти Vuex, но не дотягивает.
Увы и ах, слишком много абсолютно лишней и никому не нужной работы в рантайме, слишком много оверхеда по потребляемой памяти, а если у вас не мелкое приложение, то вы даже начнете тормоза ощущать каждый раз когда создаете MST'шный массив объектов. Так что, лучше от этого сразу же отказаться, чтобы не выпиливать его на фишишном пути проекта по причинам тормозов и прожорливости.
Ну и самая вишенка на торте, это то, что всё приложение падает когда вам от сервера не придет какое-то поле или придет не в том формате.
Пробовал его только в одном среднего размера приложении (админка для внутренних нужд компании). Данные получал с graphql так что типы были точно те что нужно. На страницы с большим количеством записей был настроен paginate.
В итоге самые большие тормоза были как раз на стороне сервера который graphql готовил.
приложение падает когда вам от сервера не придет какое-то поле или придет не в том формате
А что оно должно делать по вашему? Выдавать NaN в самом неожиданном месте?
А что оно должно делать по вашему? Выдавать NaN в самом неожиданном месте?
1) Пользователь может принципе не дойти до того места где используется параметр, который пришел не в том формате, но из-за MST увы у него все сломается в любом случае.
2) Для отображения скажем кол-ва чего-то для view слоя без разницы, скормят ему 99 или «99».
3) Этот параметр мог вообще не использоваться в принципе у вас в приложении, но на всякий случай был добавлен в описание, или раньше использовался, а теперь перестал.
Я считаю эти аргументы очень весомыми в пользу того, что не нужно крашить приложение просто так на ровном месте.
Пользователь может принципе не дойти до того места где используется параметр
Эмм, тогда зачем спрашивается его тянуть сейчас, хотя он может быть и не нужен.
Для отображения скажем кол-ва чего-то для view слоя без разницы, скормят ему 99 или «99».
Отлично а потом в таблице попросят вывести поле "итого" и 99 рублей + 5 рублей превратятся в 995 с прямо у офигевшего покупателя на глазах, а у некоторых творческих личностей это еще и уйдет на сервер, спишется с карты бедного пользователя...
Этот параметр мог вообще не использоваться в принципе у вас в приложении, но на всякий случай был добавлен в описание, или раньше использовался, а теперь перестал.
В таком случае может стоит иногда все-таки делать рефакторинг?
А что оно должно делать по вашему? Выдавать NaN в самом неожиданном месте?
Выводить ошибку в этом неожиданном месте, но не ронять всё приложение.
Собственно для этого необходимо использовать try-catch который и отловит ошибки на нужном месте. Приложение падает если такой обработчик не обнаружен и это будет работать и на mobx и на react, да хоть на php или javaEE.
Если ошибка все-таки проскочила все обработчики ошибок — то путь лучше приложение упадет (и я в sentry буду знать где и как оно упало) чем продолжает некорректную работу с неизвестным результатом и это касается любой программы на любом языке
Я предпочитаю придерживаться этой идеи и избегаю прямого обновления стора из другого стора. Придерживаюсь того, что обновление стора может производиться только из сайд-эффектов (или экшенов, в зависимости, от терминологии). В описании Flux говорилось подобное.
Хотя не, в сайд-эффектах я иногда из сторов считываю данные, а не из компонентов пробрасываю. Так что у меня тоже не однонаправленный поток данных)
Как говориться гладко не бумаге но забыли про овраги, то есть в идеале то да — нужен однонаправленный поток данных, чистая архитектура и оптимизация.
Но в итоге у всех не однонаправленные потоки данных, жирные контроллеры на бэке, архитектура которой проще нафиг уронить контейнер и заново его поднять и попробовать снова, и отсутствие оптимизации во всем. И эта проблема всего мира.
Бизнес хочет — бизнес получит. И к сожалению с этим либо придется смириться.
Либо ввести допуски и не допускать неквалифицированных разработчиков к разработке вообще.
Отклонять все требования бизнеса которые не ложатся на архитектуру (либо каждый раз менять архитектуру на нужную) и получить в итоге стоимость одной программы на уровне крыла от боинга и скорость разработки в годы.
Плюс регулярно проводить аудит технологий каждого бизнеса независимой организаций (которая будет проверять все от чипов до кода программы) и штрафовать, штрафовать, штрафовать — причем делать это всем миром.
Например если бы такой аудит вовремя бы провели у печально известных toyota и boing то это спасло бы сотни если не тысячи жизней.
Почему-то redux больше нигде не используется, кроме как в react.
NgRx для Angular — это из популярного.
FlutterRedux для Flutter и ReSwift для Swift из менее популярного.
расскажу о том, почему мы выбрали MobX
Поздравляю, вы сделали первый шаг в сторону $mol, где философия реактивности похожа, но более продвинута, а ваш первый пример выглядел бы так:
$my_app $mol_view sub /
<= Count $mol_view sub /
<= count?val 0
<= Increase $mol_button_minor
click?event <=> increase?event null
title @ \Increment
<= Decrease $mol_button_minor
click?event <=> increase?event null
title @ \Decrement
namespace $.$$ {
export class $my_app extends $.$my_app {
increase() {
this.count( this.count() + 1 )
}
decrease() {
this.count( this.count() - 1 )
}
}
}
Может показаться, что кода не особо меньше, однако, в вашем коде не хватает классов для навешивания стилей, идентификаторов для привязки автотестов, привязки системы сбора статистики и локализации. Когда прикручиваешь всё это к реакту объём кода вырастает в несколько раз. А тут всё это есть уже из коробки. Более того, в $mol не нужны такие костыли:
@action getPosts = async () => {
this.isLoading = true
this.posts = await api.getPosts()
this.isLoading = false
}
Тем более, что такой код плохо работает, когда пользователь дважды запускает экшен: индикатор загузки может залипнуть или пропасть раньше времени. Да и никакой обработки ошибок я тут не вижу. В $mol загрузка данных с индикатором загрузки и обработкой ошибков выглядела бы как-то так:
getPosts() {
return this.api().getPosts()
}
В итоге каждый проект на Redux — множество чужих спорных решений с модными на тот момент библиотеками. Получается «Франкенштейн», поддерживать который приходится вам, а не тому, кто его создал.
Это как бы философия всего Реакта, а не отдельно Редакса.
Азат Резентинов в своем докладе рассказывал, как увеличилась производительность его команды после внедрения MobX. А вот еще один отзыв, его автор утверждает, что скорость разработки увеличилась в три раза.
А представьте на сколько увеличится ваш перфоманс, если решитесь и от Реакта избавиться..
MobX — это просто FRP
MobX — это всё же ОРП.
Единственный реальный минус MobX — он дает вам слишком много свободы
Давайте я вам нормальных минусов накидаю:
- Не поддерживает асинхронные реакции — привет костылям, как в Редаксе.
- Не поддерживает нормально SuspenseAPI. Поддержать его совсем не сложно, но автор упирается.
- Не умеет реконцилировать значения, чтобы сохранялись ссылки на объекты, если они структурно не поменялись.
- Относительно тяжёлый для своей функциональности.
- Замороченное, постоянно меняющееся апи.
Одна наблюдаемая переменная может обновлять другую, которая загружает данные от третьей. Чтобы избежать этой каши, мы решили не наследовать стор от стора.
Не понял, а в чём каша-то? Если вам нужна такая логика, где один стор зависит от другого, то добавление третьего лишь внесёт дополнительную сложность. Ну и наследование тут ни при чём.
Импортируем Store напрямую и получаем все бонусы от IDE, такие как автокомплит и статический тайпчекинг.
Хех, в Реакте приходится выбирать между инверсией зависимостей и статической типизацией? А и то и другое одновременно нельзя? Как вы тестируете-то это всё без возможности замокать стор?
В свое время Redux победил, потому что был разумной альтернативой императивному jQuery.
Redux много хайпили и многие на этот хайп повелись. Теперь во многих компаниях много этого легаси, написанного вчерашними студентами на гироскутерах. Редакс никогда ни чему не был адекватной альтернативой. И любой толковый программист это понимал и приходя в такой проект первым делом выпиливал. Ну или хотя бы пилил свою обёртку над этим цирком. Так что о какой такой победе речь?
Поздравляю, вы сделали первый шаг в сторону $mol
Звучит как угроза...
Не умеет реконцилировать значения, чтобы сохранялись ссылки на объекты, если они структурно не поменялись.
А как это делает $mol? Рекурсивно пробегает вглубь по всем полям в поисках первого отличия? Как-то шибко дорого. Или это работает по-другому?
Распространять обновления далее и делать те же проверки (или даже сразу сайдэффекты) уже в зависимых вычислениях будет не менее дорого, поэтому лучше сохранять ссылки неизменными, если результат всё-равно не поменяется.
будет не менее дорого
Всё так, кроме одного жирного НО. Это будет дороже ТОЛЬКО тогда, когда объект и правда не изменился. А что прикажете делать во всех остальных случаях?
На всякий случай уточню — оно у вас и правда в глубину всё сверяет, как я выше предположил, или всё не так… спорно? Я к тому, что внутри может быть что-нибудь весьма объёмное.
А часто ли дерево данных меняется всё целиком?
Да, правда. Что, например, объёмное?
А часто ли дерево данных меняется всё целиком?
Не понял, а зачем ему меняться целиком. Достаточно одного поля в глубине.
Что, например, объёмное?
Да, что угодно. Какой-нибудь большой граф данных на десяток другой мегабайт. Зависит от задачи. В случае какого-нибудь редактора школьных расписаний там могут быть тысячи значений. В случае графического или текстового редактора десятки тысяч. Я делал на KnockoutJS визивиг-редактор документов, который пережёвывал документы под 1000+ А4 листов. Короче говоря, был бы инструмент, задача найдётся.
Да, правда.
- А если внутри observable его поля в глубину тоже observable — есть ли какие-нибудь оптимизации\проверки, чтобы хотя бы их не сверять в глубину без нужды?
- Это поведение как-нибудь отключается? Я не знаю, какой-нибудь флаг типа
reconcilateOnChange: boolean
?
Достаточно одного поля в глубине.
Ну вот и обновятся лишь ссылки на объекты по пути до этого одного поля. А все остальные не изменятся и соответственно не будет никаких зависящих от них вычислений.
В случае какого-нибудь редактора школьных расписаний там могут быть тысячи значений. В случае графического или текстового редактора десятки тысяч
Такие графы нет смысла хранить JSON деревом. Лучше иметь реестр узлов, а в узлах связи вида "родитель", "лидер", "список детей". В не POJO объекты $mol_conform конечно по умолчанию не лезет — они сравниваются по ссылке. Но можно задать хендлер для своего класса. Соответственно, если хочется выключить это поведение — достаточно вернуть не POJO значение.
В не POJO объекты $mol_conform конечно по умолчанию не лезет — они сравниваются по ссылке
В смысле по ссылке? Ссылка же останется неизменной при изменении значения. Мутабельность же. Или мы о разных вещах разговариваем. Я имел ввиду что алгоритм реконсиляции мог бы проверять, наткнувшись на, observable поле, не все его данные в глубину, а просто дёрнуть какой-нибудь флаг _changed
или скажем сравнив номер версии. Ну это уже зависит от имплементации. Тогда части сравнений можно было бы избежать.
Т.е. я правильно понимаю, что у вас при изменении значения оно будет записано как изменённое, только если что-то внутри фактически было изменено, и подсунув аналогичный объект как значение, ничего не произойдёт? И что в глубину проверка будет осуществлена только для POJO значений, а любые вложенные реактивные элементы в глубину проверяться не будут?
Ссылка же останется неизменной при изменении значения. Мутабельность же.
Не, $mol_atom ничего не заворачивает в прокси, как MobX. Соответственно, если если измените в любом объекте нереактивное свойство — ничего не протрекается.
// Зависимость от части данных
@ $mol_mem
bar() {
return this.foo().b
}
// Записали данные - bar обновится и изменится
this.foo({ a : 1 , b : [ 2 , { c : 3 } ] })
// Обновили данные - bar обновится, но не изменится
this.foo({ a : 2 , b : [ 2 , { c : 3 } ] })
// Ничего не произойдёт
this.foo().b.push( 4 )
Т.е. я правильно понимаю
Агась.
Этот codesandbox дико кривой, не смог в нём завести. В реплите завёл: https://repl.it/@ninjin/molatom2#index.ts
Даже если $mol лучше по всем параметрам, я не знаю как его продать руководству и команде. Не знаю как нанимать людей на $mol. Не знаю зачем менять реально работающее «достаточно хорошее» решение, на потенциально «идеальное».
Дальше синтаксиса шаблонов $mol никто в моей команде не будет смотреть. <=> — что это такое? $.$$ это мои удивленные глаза.
Как вы тестируете-то это всё без возможности замокать стор?
Jest позволяет замокать любой импорт. Более того, он может замокать любую функцию (даже не импортируемую) любого модуля. Сейчас ведь не 1994 год, в самом деле.
Не знаю зачем менять реально работающее «достаточно хорошее» решение, на потенциально «идеальное».
Вот так и говорите что вас всё устраивает, а не "не знаю как продать".
Дальше синтаксиса никто в моей команде не будет смотреть.
Ну и команду вам там набрали..
Jest позволяет замокать любой импорт. Более того, он может замокать любую функцию (даже не импортируемую) любого модуля.
Вы про манкипатчинг, который может вообще всё разломать? А так, тесты — лишь одно из многих применений инверсии контроля.
Дальше синтаксиса никто в моей команде не будет смотреть.
Ну и команду вам там набрали..
Ну вообще-то синтаксис имеет огромное значение, читаемость, легкость написания, восприимчивость с первого взгляда и дальнейшая поддержка крайне важны, все это отсутствует у $mol'a, так что синтаксис шаблонов $mol увы рассчитан на вас одного)
const counter = useStore($counter)
вместо того чтобы просто сразу использовать переменную counter там где нужно, без предварительной ручной подписки в каждом компоненте который будет ее использовать.
Как по мне, многословность redux существенна только в маленьких проектах. В примере, где логики две строки — да, многословно, в реальном коде — терпимо.
Мне нравится редакс именно за разделение на экшены, редьюсеры и сайд-эффекты. Код значительно медленнее превращается в бардак.
МобХ опять сводит все это в одно место.
Самое неприятное место для меня был connect. Сейчас его заменили хуками — и это совсем другое дело, наконец-то можно пользоваться без боли и проблем с типизацией.
А что из себя представляют редьюсеры и чем более простым заменяются я написал в этом комменте.
я бы сказал ровно наоборот — в маленьких проектах эта многословность туды-сюды, а в больших каждое добавление редуктора, создание connect-та с мапингом диспатчер/стейта выглядит, как то, что кто-то с больным воображением и садистским уклоном решил сделать штуку, которую распиарили на столько, что людям даже понравилось!) (не, я понимаю что редакс всяко лучше флюкса, а то были первые нервные шаги в «тру-реактивности» на большом рынке, но выглядит именно так).
Проще: любые правки в «сторе» (т.к. строго говоря стора в Redux нет) это боль. И чем больше аппка — тем больше боли т.к. как ни крути, а веб-апп это большаяя трансформация данных на событиях.
Код значительно медленнее превращается в бардак.
В случае типичного old-school Redux изначальное состояние кода уже бардак. Но вот технический долг может копиться медленнее, чем в случае observable решений. Однако, имхо, стоимость разработки получается дороже для проектов любой сложности. И чем динамичнее меняется проект, тем больше по нему бьёт Redux, т.к. он заставляет разводить в коде просто чудовищное количество бюрократии. И большая часть этой бюрократии там нужна "на всякий случай".
Например на кой чёрт нужна модель Pub Sub, из-за которой все строчат свои switch-case и из-за которой главный reducer имеет O(n)
? А сделано это для того, чтобы на 1 action можно было подписаться в разных частях приложения (крайне спорное решение с точки зрения как чистоты кода, так и архитектуры в целом). В проекте может лишь пару раз возникнуть такая потребность, или не возникнуть вовсе, на народ упорно строчит reducer-ы и action-ы и action-type-ы отдельно. В худшем случае в разных файлах и тогда к этому всему добавляются ещё и тонны export-ов и import-ов. В итоге даже минимальные изменения в коде превращаются в "я задел 12 файлов" и везде что-нибудь поправил. В то время как в менее бюрократичных системах это может быть 1-2 строки, которые не оторваны друг от друга.
Да и вообще сама идея оторвать чертовски связанные друг на друга вещи и раскидать их по кодовой базе это ооочень спорная штука. Да ещё и привязать это всё к одной глобальной переменной. Теперь те вещи которые друг от друга никак не зависят связаны единой глобалкой. А те вещи, которые было бы удобно держать близко, разбросаны как попало и любые изменения напоминают стрельбу из миномёта.
И если это не бардак, то что это?
Самое неприятное место для меня был connect
А это вообще замечательная бомба с O(n)
:) Мало того, что reducer вызывается в глубину на любой чих, так ещё и mapStateToProps вызываются все и всегда. Просто замечательно масштабируемая штука… Это была ирония.
Сейчас его заменили хуками — и это совсем другое дело, наконец-то можно пользоваться без боли и проблем с типизацией.
Т.е. изначально концепция была: ай-ай-ай, не мешайте презентацию и бизнес-логику в одном компоненте. Разделяйте их на глупые и умные компоненты! А теперь — уау! хуки! к чёрту connect-ы, теперь будет сгребать всё в одну кучу и руками вызывать dispatch-и. Что случилось, то? :)
все строчат свои switch-caseВот прямо все? И никто не использует handleActions()?
Последнее время экспериментирую с mobx-keystone и это хорошая альтернатива почти не развивающемуся mobx-state-tree
useObserver
как раз лучше не использовать, потому что его поведение контринтуитивно. Эта штука только для тех, кто совершенно точно знает что делает.
Суть в том, что <Observer/>
, в отличии от useObserver
, изолирует любые рендеры внутри себя, а потому его довольно сложно использовать неправильно. Кроме того, <Observer/>
можно использовать для оптимизации рендеров, в отличии от useObserver
...
На мой взгляд, Redux это не столько библиотека, сколько набор принципов, объединяющих Flux и Elm архитектуры.
Все проблемы с бойлерплейтом и выбором инструментов для redux решаются с помощью https://redux-toolkit.js.org/. Этот инструмент разработаный той же командой и рекомендуемый на первой же странице в документации https://redux.js.org/. Жаль, что официально рекомендованный инструмент появился так поздно.
Насчет "много магии в MobX" я могу объяснить. Redux очень предсказуемый, его исходники можно изучить за 30 минут, исходники MobX, для меня, выглядят сложнее. Глубокое понимание работы инструмента внутри, позволяет чувствовать себя увереннее при работе с ним.
В статье говорится, что MobX сторы что-то вроде сервисов в Angular. Однако в Angular также часто используют аналог redux https://ngrx.io/.
MobX выглядит привлекательно, нет бойлерплейта, но я всё-таки согласен с мыслью, что смешивание асинхронности и мутаций делают систему мение детерминированной. В Redux можно легко понять причину любого race condition, если получилось воспроизвести его хотя бы один раз. Если же мешать асинхронность и мутации в одну кучу, может сложиться ситуация когда дебажить придется долго.
С появлением starter kit, я на данный, момент не вижу преимущества перехода с Flux на MV* и с Redux на MobX. Может быть кто-то из вас сможет меня переубедить, но эта статья не убедила :)
Все проблемы с бойлерплейтом и выбором инструментов для redux решаются с помощью redux-toolkit.js.org.
Смею предположить, что далеко не все, а только самую лютую боль от редакса.
Жаль, что официально рекомендованный инструмент появился так поздно.
Вы видите в этом «жаль», я вижу в этом изначально кривую архитектуру редакса, на которую нужно дополнительно намазывать код просто для того, чтоб болело не так сильно. И автор редакса эту боль осознал только после того, как редакс ушел в массы, его личной квалификации для этого не хватило.
Насчет «много магии в MobX» я могу объяснить. Redux очень предсказуемый, его исходники можно изучить за 30 минут, исходники MobX, для меня, выглядят сложнее. Глубокое понимание работы инструмента внутри, позволяет чувствовать себя увереннее при работе с ним.
Leftpad тоже очень предсказуемый. Это не означает, что ваш код станет прям сильно лучше, если вы начнете использовать leftpad. С редаксом то же самое — его внутренняя простота это плюс, но плюс крайне малозначимый на фоне полезности фич, которые он даёт.
В статье говорится, что MobX сторы что-то вроде сервисов в Angular.
Сторы MobX — это буква M в архитектуре MVC. Сервисы в ангуляре — это штука, связывающая M с другими буквами. В случае с MobX штукой, связывающую M с прочими частями — будет сам MobX.
В Redux можно легко понять причину любого race condition, если получилось воспроизвести его хотя бы один раз.
«Если воспроизвести» — это вообще не аргумент. Инструменты для логгинга есть везде, что в редаксе, что в mobx. С подробными логами «легко понять причину» вообще нигде не вопрос.
M — это данные (скорее всего с бэкенда) + логика (скорее всего сервисы),
VM — observable поля (состояния вью, обычно сторы)
V — понятно
Весь смысл MV* архитектур в том что букву M можно переиспользовать в дугих приложениях и в теории даже заменить react на angular, например. Другое дело, что на клиенте это скорее всего излишне и поэтому зачастую store = VM + M
MVVMS (services + controllers)
Не будем говорить о данных с сервера.
M — observable (поля стора)
VM — calculated (то что в редаксе делают redusers, подготовка данных для конкретных view
V — реакт и т. п.
S — actions — тут как раз и происходит работа с сервером и обновление моделей
import CounterStore from "./Counter"
const App = observer(() => {
return {CounterStore.count}
})
На NodeJs при прямом импорте стора один и тот же инстанс будет использован при всех вызовах компонента.
import CounterStore from "./Counter"
const App = observer(() => {
return <h1>{CounterStore.count}</h1>
})
Получается, такой подход можно использовать только, если нет SSR на NodeJs, когда асинхронно может обрабатываться несколько одновременно поступивших запросов. Нет?
Тогда не надо писать никаких длинных портянок
Кажется команда Redux поняла наконец-то что они куда-то не туда повернули в своей жизни, но чтобы сохранить лицо они пытаются впарить людям MobX (immer) под видом Redux.
они пытаются впарить людям MobX (immer)
Не очень понимаю что общего вы увидели между mobX и immer. Первое полноценный observable с трекингом зависимостей и уведомлениями. Второе просто proxy-обёртка которая переопределяет присваивания и чтения из объекта, по факту проводя их иммутабельно за кадром. immer просто позволяет вам не писать return {...prevSt, changedField: newValue}
.
Не буду утверждать, что использования import правильно. Но, вы молодцы, что не делаете как все, а думаете своей головой. Ибо best practices для реакт проектов пока очень сомнительные.
Мы в прошлом даже в angular-е заменили DI на import. Кода стало немного меньше, стало понятней, что откуда берется, пропали ошибка из-за пропуска зависимости в конструкторе или ошибки из-за очередности. Нам DI оказался не нужен. Уверен, что и в большинстве проектов он не нужен. Конечно, бывают исключения, но, имхо, большинство overengineering-ом занимаются.
Насчет:
@action getPosts = async () => {
this.isLoading = true
this.posts = await api.getPosts()
this.isLoading = false
}
вместо этого, я пока предпочитаю писать примерно так:
class PostsStore {
@action setPosts = (posts) => {
this.state.posts = posts;
this.isLoading = false;
}
// ...
}
getPostsAction = async () => { // или getPostsSideEffects
postsStore.setIsLoading(true);
const posts = await api.getPosts();
// ... - side effects
postsStore.setPosts(posts);
}
Потому-что при вызове api иногда бывает нужна дополнительная бизнес-логика и я не считаю правильным смешивать ее с логикой состояния приложения (стора). Также бывает нужно после обращения к api обновить данные в нескольких сторах.
С другой стороны, в вашем примере кода меньше и в целом, он проще.
MobX позволяет создавать вычисляемые поля от вычисляемых полей другого стора. Одна наблюдаемая переменная может обновлять другую, которая загружает данные от третьей. Чтобы избежать этой каши, мы решили не наследовать стор от стора.Как раз для похожих ситуаций, я обновляю разные сторы через getPostsAction, а не один стор через другой.
Другой момент. Я предпочитаю группировать данные стора в объект(ы). То есть частично отделять логику от данных.
PostsStore {
@observable state: {
isLoading: false,
posts: []
};
@action setPosts = (posts) => { ...
}
Преимущества такого подхода:
- можно легко передать все данные, в случае необходимости. C данными удобнее работать, когда они не смешаны с функциями.
- при использовании typescript можно легко задать тип данных, с которым стор работает.
- более простое наследование стора, т.к. наследуется только методы, но не данные.
Кстати, было бы интересно в будущем увидеть на хабре статейки о best practices в mobx, кто к чему пришел. А то в основном статьи про сравнение mobx с redux.
А как вы тестировали ваше angular приложения с импортами и вместо DI?
Ну и найдутся способы протестировать приложения с импортами.
Например, в jest: jestjs.io/docs/en/es6-class-mocks
Может с DI удобней тестировать, но я не обладаю достаточным опытом в написании тестов, чтобы сравнивать.
Не актуально, когда в последний раз писал на Redux кода у меня было меньше, чем в Ваших MobX примерах.
Пишется простой helper на 10 строк максимум на базе библиотеки redux-actions
:
import { createAction } from 'redux-actions'
const createActions(prefix, actionTypes) =>
actionTypes.reduce(
(result, actionType) => ({
...result,
[actionType.toLowerCase().replace(/_([a-z])/g, (x, y) => y.toUpperCase())]:
createAction(`${prefix}@${actionType}`)
}),
{}
)
А далее допустим есть flow загрузки новостей (в качестве примера), создаём:
- Action
- Для него функцию action-creator
На каждый action по одной строчке:
const newsActions = createActions('NEWS', [
'LOAD_REQUEST',
'LOAD_SUCCESS',
'LOAD_FAILURE',
])
В качестве ключей newsActions
получаем:
loadRequest
loadSuccess
loadFailure
В качестве значений — функции с переопределённым методом toString
, таким образом newsActions.toString()
вернёт NEWS@LOAD_REQUEST
, а newsActions(123)
вернёт { type: 'NEWS@LOAD_REQUEST', payload: 123 }
Т.е. желать меньшего, чем одна строчка на action невозможно (технически все эти 3 action-а можно и в одну строку записать и уложиться в 80 символов на строку, ещё останется). И это не только action, а сразу и action-creator.
Ваш пример:
export function increment() {
return {
type: 'INCREMENT'
}
}
export function decrement() {
return {
type: 'DECREMENT'
}
}
Превратится в:
export default createActions('INCREMENTOR', ['INCREMENT', 'DECREMENT'])
А что до редьюсеров, в лучших случаях тут тоже всё до одной строчки сводится (ключи объекта внутри квадратных скобок автоматически преобразовываются к строке через вызов метода toString
):
import { handleActions } from 'redux-actions'
handleActions({
[newsActions.loadRequest]: state =>
{ ...state, isLoading: true, isLoaded: false, isFailed: false },
[newsActions.loadSuccess]: (state, { payload: news }) =>
{ ...state, isLoading: false, isLoaded: true, news },
[newsActions.loadFailure]: state =>
{ ...state, isLoading: false, isFailed: true },
}, { isLoading: false, isLoaded: false, isFailed: false, news: [] })
Также нужно заметить, что Вы манипулируете синтаксическими ухищрениями используя ES6, но игнорируя его особенности, вот Ваш пример:
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
onIncrement: () => {
dispatch(increment())
},
onDecrement: () => {
dispatch(decrement())
}
}
}
Вместе с action-creator-ами выше это могло быть записано так:
export const increment = () => ({ type: 'INCREMENT' });
export const decrement = () => ({ type: 'DECREMENT' });
// другой файл
const mapStateToProps = state => ({ count: state.count });
const mapDispatchToProps = dispatch => ({
onIncrement: () => { dispatch(increment()) },
onDecrement: () => { dispatch(decrement()) },
});
Что уже совсем не похоже на подчёркиваемую многословность, напротив.
Но главная проблема MobX, на мой взгляд, это плохая интегрируемость с системой типов. Вы написали что у вас TypeScript среди используемых инструментов. И судя по всему ваша команда, как и подавляющее большинство пользователей TypeScript просто тратите своё время, т.к. если вы не используете мощь типизации (которую TypeScript может дать), вы просто тратите время на резрешение ошибок типов, в то время как Ваш код остаётся плохо типизированным и у вас открытое поле для багов в рантайме.
Взять к примеру то же состояние загрузки, есть у вас допустим какая-нибудь страница, где есть id условной горячей новости дня, из этого id вы генерируете запрос на сервер. Пока вы этот id ниоткуда ещё не загрузили, вам нужно туда что-то записать, ну большинство просто запишет 0
, а это в корне неверно. Если вы, полагаясь на собственную внимательность, забудете вручную проверить, загружены ли данные и/или что этот id не равен 0
, то получите рантайм баг, т.к. на сервере не будет такого элемента (и это не воображаемый сценарий, а случай из реальной жизни, из личной практики). А в это время TypeScript будет говорить вам что вы всё сделали правильно.
Это можно легко решить на TypeScript с помощью generic-ов и sum-type. Вот мой личный экспериментальный пример с Redux:
https://github.com/unclechu/typescript-redux-and-data-request-flow-proper-typing-experiment
Суть в том, что у вас будет несколько базовых generic-ов:
export type Idle<T> =
{ readonly [K in keyof T]: T[K] } & { readonly status: 'idle' };
export type Request<T> =
{ readonly [K in keyof T]: T[K] } & { readonly status: 'loading' };
export type Success<T> =
{ readonly [K in keyof T]: T[K] } & { readonly status: 'success' };
export type Failure<T> =
{ readonly [K in keyof T]: T[K] } & { readonly status: 'failure' };
export type CommonFailure =
Failure<{ errMessage: string }>;
// Самый главный, подходящий для большинства случаев, когда специфичный payload
// есть только для case-а успешной загрузки данных с сервера.
// А для ошибок только текстовое представление ошибки.
export type DataRequest<T> =
Idle<{}> | Request<{}> | Success<T> | CommonFailure;
С помощью пары helper-ов можно проверять исчерпываемость switch-case проверок, а также ограничивать scope проверки action-ов в reducer-ах, т.е. action-ы должны быть обработаны эксплицитно все, но только те, которые имеют отношение к конкретной ветке store-а (можно почитать комментарии к коду по ссылке выше, они оттуда, а также посмотреть примеры использования для большей ясности):
export const ProveExhaustiveness =
<T extends never>(x: T): void => {}
export const ImpossibleCase =
<T extends never>(x: T): never => {
throw new Error(`Unexpected case, value: "${x}"`)
}
Выброс исключения в ImpossibleCase
как правило на самом деле никогда не происходит (если не было использовано магии с any
), т.к. type-checker проверяет, что до этой точки выполнения невозможно дойти.
Таким образом можно описать reducer как (можно также смело полагаться на строки в качестве action-type, т.к. type-checker сразу выпадет с ошибкой на опечатку):
interface NewsItem {
readonly date: string
readonly title: string
}
type NewsState = DataRequest<{ news: ReadonlyArray<NewsItem> }>
type NewsAction =
| { readonly type: 'NEWS@LOAD_REQUEST' }
| {
readonly type: 'NEWS@LOAD_SUCCESS'
readonly news: ReadonlyArray<NewsItem>
}
| {
readonly type: 'NEWS@LOAD_FAILURE'
readonly message: string
}
const newsReducer =
( state: NewsState = { status: 'idle' }
, action: NewsAction
): NewsState => {
switch (action.type) {
case 'NEWS@LOAD_REQUEST':
return { status: 'loading' }
case 'NEWS@LOAD_SUCCESS':
return { status: 'success', news: action.enws }
case 'NEWS@LOAD_FAILURE':
return { status: 'failure', errMessage: action.message }
default:
ProveExhaustiveness(action)
return state
}
}
ProveExhaustiveness
тут проверяет только на уровне типов, в runtime любой action проходит через все reducer-ы. Но в данном случае ограничивается подмножество action-ов, которые могут быть обработаны в данной конкретной ветке стора (намеренно, это необязательно, просто показана такая возможность, и всё во время type-checking-а).
А уже в компоненте потом можно просто проверить в начале:
if (news.status !== 'success') {
if (news.status === 'failure') return <div>We are fucked up: {news.errMessage}</div>;
else return <div>Loading</div>;
}
И всё, дальше уже type-checker-ом будет доказано, что news
— это Request<{news: ReadonlyArray<NewsItem>}>
. Любые опечатки или обращения к поляем другого типа из общего sum-type-а в то время, как доказано что это другой тип (либо не доказано, что тот, к полю которого обращаемся, либо не исключены проверками типы, у которых такого поля с соответствующим типом этого поля нет), — приведут к ошибке type-checking-а. То, что доктор прописал, машины делают за людей их работу и ловят баги!
Под всё это ещё можно и генерализованный компонент сделать, который будет в одну строчку обрабатывать ошибку или состояние загрузки компонента. Т.е. как-то вроде этого:
if (smth.status !== 'success') return <Failover thatSumType={smth}/>;
А теперь попробуйте переложить эту модель на MobX и тут же поймёте почему это на MobX ложится плохо. Потому что при описании свойств объекта MobX мы имеем фиксированную модель данных, и обернуть её целиком в DataRequest
не получится, а получится лишь оборачивать каждое зависимое от состояния загрузки с сервера данных поле в такой generic. В итоге проверять в компоненте на состояние загруженности данных уже придётся каждое поле, к которому обращаемся, хотя они по смыслу сгруппированы. И то вышеописанное для примера поле id горячей новости дня, и список новостей и всё остальное, что зависит от наличия данных, полученных от сервера.
P.S. Местами где-то сумбурно и поверхностно написал, просто тут материала можно на целую статью выделить, если расписывать в деталях.
Пример на MobX как это могло бы выглядеть:
class NewsStore {
@observable public todayHotNewsId: DataRequest<{ id: number }> =
{ status: 'idle' };
@observable public news: DataRequest<{ list: ReadonlyArray<NewsItem> }> =
{ status: 'idle' };
public fetchNews = (): Promise<void> =>
Promise.resolve()
.then(async () => {
if (this.news.status === 'loading') return Promise.resolve();
await action(() => {
this.news = { status: 'loading' };
});
await action((response: AxiosResponse<{
readonly news: ReadonlyArray<NewsItem>
readonly hotId: number
}>) => {
this.news = { status: 'success', list: response.news };
this.todayHotNewsId = { status: 'success', id: response.hotId };
});
})
.catch(
action((err: Error) => {
this.news = { status: 'failure', errMessage: err.message };
this.todayHotNewsId = this.news;
})
);
}
В качестве варианта можно рассмотреть сборку всех зависимых данных в одно свойство, но тогда это уже будет не MobX, и все оптимизации observable-ов будут рушиться, будет одно большое значение, которое будет постоянно меняться.
class PostsStore {
@observable isLoading = false;
@observable posts: IPostItem[] = null;
@observable error: string = null;
getPosts = async () => {
this.isLoading = true;
try {
this.posts = await api.getPosts();
this.error = null;
} catch (e) {
this.error = e.message;
} finally {
this.isLoading = false;
}
};
}
Какие тут проблемы то?
this.news = { status: 'success', list: response.news };
Это у вас привычка после redux reducer-ов всё хранить в одной переменной осталась? :) Или это наследие .setState
?
На хуках всё тоже самое:
import { apiFetchPosts } from '~/api';
const Posts = () => {
const { result: posts, isFetching, error, fetch } = useFetchRequest(apiFetchPosts);
useOnMount(fetch);
return <div /* some TSX code *//>;
}
Сам хук в общих чертах такой:
const useFetchRequest = <TArgs extends unknown[], TResult>(
method: (...args: TArgs): Promise<TResult>
) => {
type TState = {
isFetching: boolean;
result: TResult | null;
error: TAnyError | null;
};
const [state, setState] = useImmerState<TState>({
error: null,
result: null,
isFetching: false,
});
const fetch = useCallback((...args: TArgs) => {
try {
setState({ isFetching: true, error: null });
return await method(...args);
} catch (error) {
setState({ error });
} finally {
setState({ isFetching: false });
}
}, [method]);
return { ...state, fetch };
};
Нюансы:
- На самом деле туда ещё нужно добавить возможность отмены предыдущего запроса, в целях избегания race condition
- Результат можно и не хранить (а оставить его, скажем, в redux, если очень нужно)
- Всё прекрасно типизируется
- Всё прекрасно переиспользуется. В частности в запросах с пагинацией с курсорами пишется хук поверх текущего, который умеет уже в пагинацию
- В серьёзных проектах требуется плотно мемоизировать (для redux это тоже актуально)
- Я выше писал про "не хранить в 1 переменной" и само сделал тоже самое. Но это в целях оптимизации широко используемого хука (у нас подобный встречается раз 70 в коде)
Что такого даёт Redux чтобы писать на нём вместо того что выше?
- Redux Dev Tools? Ну такое себе. Он мне никогда не нравился
- Возможность получить слепок всего стейта приложения? Это плюс, да, но по факту с течением времени начинаешь хранить в redux всё меньше и меньше стейта, т.к. он зверски неудобный, и этот плюс уходит на нет.
- Однонаправленный поток данных? В какой-то степени он и здесь есть, просто он не на всё приложение с глобальной переменной, а на отдельно взятый модуль\блок\whatever.
- Прозрачность выполнения последовательности действий и изменений стора? Да, этого тут нет. Увы. Ковырять React Dev Tools в поисках зарытов в подхуках от подхуков стейта неудобно
- Сериализация\десериализация всего приложения? Редкий кейс, но да, тут хардкорный redux выигрывает (правда это весьма не тривиально организовать)
Это у вас привычка после redux reducer-ов всё хранить в одной переменной осталась? :) Или это наследие .setState?
В данном случае мы видим два сильно связанных значения, изменяющиеся атомарно. Вполне логично хранить их вместе.
Три :) там ещё errMessage
. Но согласен, в какой-то мере это дело вкуса.
Это не дело вкуса, и тут не «всё в одном», а всего-лишь самый необходимый минимум для того, чтобы на уровне типов отражалось доступность значения. Т.е. это по сути технически возможный минимум данных (опять же, в контексте корректно-типизированных свойств). На уровне типов у вас прямая связь с состоянием загрузки значения, либо ошибкой, произошедшей во время загрузки.
Всё прекрасно типизируется
Поле с ошибкой хранится в стороне, никак не связано с самим значением на уровне типов. Не могу сказать, что это самый прекрасный способ описать типы.
Поле с ошибкой хранится в стороне, никак не связано с самим значением на уровне типов
Ничего не понял, если честно. Как any
ошибка может быть завязана на какой бы то ни было тип значения? А ошибка может быть только any
, по понятным причинам (мы не управляем источником ошибок).
Если же вас смущает, что она не string
(допустим нужна некая принудительная унификация), то опять же я не вижу никакой привязки к решению. Пишете helper аля anyErrorToStringError
и используете либо по месту, либо прямо в хуке.
Наш хук (в реальном проекте) принимает не только метод а груду параметров (к примеру fetchOnMount: true
). Никто не мешает добавить свои согласно нуждам.
В общем, я вас не понял.
А ошибка может быть только any, по понятным причинам (мы не управляем источником ошибок).
Да нет же, она может иметь строковое представление, как в примере, может быть инстансом Error, может иметь и какую-то более сложную модель данных, и вообще состоять из sum-type-а. Если ошибка имеет тип any
, то это плохо сделанная работа. А если считаете, что это ок, то мой совет: выкидывайте TypeScript и не тратьте на него время. Когда он используется таким образом от него больше вреда, как от time-spender-а, чем пользы.
Если же вас смущает, что она не string
Исходя из его был сделан такой вывод? Смущает. Точнее смущает если она не имеет конкретного описанного типа.
Если ошибка имеет тип any, то это плохо сделанная работа
Не занимайтесь самообманом. Ошибкой вы не управляете. В TS не спроста она принудительно распространяется как any (хотя могли и unknown воткнуть, на самом деле, кажется под это дело завезли какой-то флаг).
Лучшее что вы можете сделать, это привести ошибку к нужному вам виду через guard. Мы как раз их и используем. А писать
} catch(err /* any */) {
state.error /* : MyError */ = error;
}
это не про typechecking, это про type-casting :)
Или вы имеете ввиду нечто вроде:
type TResult<T> =
| { result: T, error?: never }
| { result?: never; error: TAnyError }
? Если да, то хуки вас ни в чём не сдерживают. Можно реализовать и так (но я бы не стал).
P.S. не считаю что ошибка и результат сильно связаны. Но это уже предмет совсем другой дискуссии, конечно.
Про «не актуально» в начале это относилось к:
Наверное, всем надоели завывания про многословность Redux, но это реальная проблема.
Случайно стёр цитату.
не актуально
Актуально. Да хоть сколько его сокращай, всё равно груда boilerplate-а. Для примера гляньте на Svelte. Ну просто небо и земля :)
А самое веселье начинается когда нужны переиспользуемые компоненты, которые хранят state внутри redux, но должны работать в разных местах, может быть даже одновременно. Тут такие пляски с типами и фабриками начинаются… Вплоть до написания своей версии connect
.
export function increment() {
return {
type: 'INCREMENT'
}
}
export function decrement() {
return {
type: 'DECREMENT'
}
}
вместоexport const increment = () => ({ type: 'INCREMENT' });
export const decrement = () => ({ type: 'DECREMENT' });
Потом обязательно руками мапить каждый вызов Диспатча, вместо того, чтобы воспользоваться встроенным bindActionCreators и замапить всё сразу в одну строку. Также обязательно поплеваться на свитч-кейсы, хотя для того, чтобы вместо них сделать словарь — не нужно даже redux-actions c его handleActions — можно просто написать словарь, а потом вызвать из него нужную функцию по имени action-type'а. Можно даже просто экспортировать из файла редьюсеры именованными объектами, если сразу именовать их типом action'а.
const createActions(prefix, actionTypes) =>
Вы это затем как-нибудь типизируете?
handleActions({
[newsActions.loadRequest]: state =>
{ ...state, isLoading: true, isLoaded: false, isFailed: false },
Сюда бы immer, а то всё ещё очень страшно выглядит. Особенно если потребуется вложенные объекты менять.
export type Request<T> =
{ readonly [K in keyof T]: T[K] } & { readonly status: 'loading' };
А затем шаг номер 2: перестать хранить в Redux всякую ерунду вроде статуса загрузки, и перенести это в хуки (аля const { isFetching, error, isRequestComplete, result: posts } = useRequest(anyAsyncFunc)
). Ваша кодовая база радикально похудеет, всё станет проще и понятнее. Можно даже оставить redux, но уже не для временных данных вроде анимаций, статусов загрузок и ошибок.
Вообще redux, конечно, удручает тем, что любой опытный разработчик пытается победить его многословность используя всякую чёрную магию вплоть до кодогенерации в babel/ts plugin-ах, а она зараза, всё равно избыточная.
С течением последних лет я постепенно всё больше и больше сокращал redux-boilerplate, и в итоге всё равно пришёл к тому, что пока я вообще не использую redux, кода меньше и он адекватнеепонятнее. В итоге стал хранить в redux исключительно то, что нужно сразу в нескольких модулях сразу. Т.е. shared state. В общем наигрался. Рекомендую ;)
Вы это затем как-нибудь типизируете?
В первой части рассматривался пример без типизации. Если нужна типизация, хорошая, а не абы какая, часто приходится где-то жертвовать краткостью, не обязательно в данном конкретном случае. На мой взгляд строгая корректная модель типов гораздо важнее предельной краткости. По-этому я бы вообще не стал сравнивать самый короткий код и самый хорошо типизированный. Т.к. всякие хитрые helper-ы пересобирающие объекты из одного вида в другой порой просто невозможно корректно описать в рамках TypeScript.
Сюда бы immer, а то всё ещё очень страшно выглядит. Особенно если потребуется вложенные объекты менять.
Я бы immutable.js использовал, но для простоты примерра намеренно опустил любые доп. библиотеки.
А затем шаг номер 2: перестать хранить в Redux всякую ерунду вроде статуса загрузки, и перенести это в хуки
Если я к примеру не хочу повторно загружать данные при отмонтировании компонента, то это не подходит. Если я хочу иметь доступ к данным из разных компонентов, то тоже не подходит. И вообще, мне не нравится идея нагружать компоненты логикой загрузки данных откуда-то, предпочитаю делать это в стороне от компонентов (View). От компонентов должен только поступить сигнал «хочу данные».
Т.к. всякие хитрые helper-ы пересобирающие объекты из одного вида в другой порой просто невозможно корректно описать в рамках TypeScript.
Да. Получается такой зверски сложный мета-код, что дебажить его смерти подобно. У нас хватает такого добра. Где 100 строк типов, а 15 строк кода.
Я бы immutable.js использовал, но для простоты примерра намеренно опустил любые доп. библиотеки.
Не думал что кто-то ещё использует это. Возьмите immer, не мучайте себя и людей :)
Если я к примеру не хочу повторно загружать данные при отмонтировании компонента, то это не подходит
Если я хочу иметь доступ к данным из разных компонентов, то тоже не подходит
Да. Всё так. Всякой задаче свой инструмент. Часто у вас возникает такая задача? Вот я сужу по нашим проектам — если компонент был отмонтирован, а потом смонтирован заного, то сие произошло не случайно. И те данные устарели, мы лучше покажем прелоадер, чем будет провоцировать UI не совсем уж на консистентную ситуацию.
Но согласен с тем, что бывают задачи, когда стор должен быть внешним. Если эта задача не стоит слишком часто, то это решается на уровне context-провайдера. Он тоже замечательно типизируется.
Мы стали использовать redux в новом коде только для хранения нормализованных сущностей, которые используются повсеместно. То самый случай когда от redux есть реальная осязаемая польза.
А что, и доступ к данным из разных компонентов у вас бывает редко?…Если я хочу иметь доступ к данным из разных компонентов, то тоже не подходит…Часто у вас возникает такая задача?
А что, и доступ к данным из разных компонентов у вас бывает редко?
Очень сильно зависит от сути проекта. Могу вспомнить и такие где connect-ы были повсеместно, т.к. всё приложение было завязано на одни и те же данные. Могу вспомнить и такие где каждый модуль\блок сам по себе. И гибридные, где все модули сами по себе, но если они будут переиспользовать единый реестр общих сущностей, то UI будет лучше консолидирован. В итоге — каждому приложению свой рецепт.
Проще всего писать приложение в котором нет глобального стейта. Либо это всё отдано на откуп framework-у, как это сделали в различных реализациях GraphQL на стороне клиента.
2. Многословность никуда не делась: у вас всё ещё есть события, редуктор и соотноситель (на сленге actions, reducers, mappers), это всё избыточная писанина, которая может создать иллюзию понятности и подконтрольности (вроде иллюзии магии MobX), но на деле это провоцирует неокрепшие умы в условиях дед-лайнов писать… Кастомный хук, вызывающий кастомный хук, который вызывает хук, который вызовет первый кастомный хук (что вылилось в некоторые финансовые проблемы в дальнейшем).
3. Ценность MobX, как инструмента, в том, что он позволяет сконцентрироваться на том, что происходит в хранилище (store), и незатейливо выводить состояние дел, за счёт вычитания из действия разраба лишней долбанки по созданию сущностей входа, обработки и связывания для обеспечения реактивности.
4. Про типы: ваше владение типизацией круто, без шуток, выглядит мудрёно (для меня), но, вроде, хорошо. Но при чём тут тип хранилища? Я не часто юзаю такие сложные generic (ни к чему, простота мне дороже), а switch-case конструкциям предпочитаю hash-map-function вызовы, но типоскрипт и MobX живут друг с другом весьма отлично, можно использовать на всю, можно на минималках. Всё вышеописанное можно 1-в-1 перенести на свойство объекта в сторе MobX и экшн. Вообще без правок кода, по сути (ну this. добавить, ок). Задача выполнена, реактивность будет жить)
5. Не стоит приравнивать state из Redux и store из MobX. Это разные инструменты, которые могут делать одно и то же, а могут и разное (как ключ-трещётка и ключ рожковый, возможно). В state с Immutable возможна «машина времени». В store с декораторами возможен state+action+reducer+mapper в одном. В state мы можем творить любую дичь и передавать дальше совершенно оторванный от изначальных данных state. В store мы можем следить за консистентностью на каждом шаге экшена (нет, state нам этого не гарантирует, даже Immatable не обещает, всё зависит от структуры стейта на выходе о принятых обязательствах разрабов в т.ч. типоскриптовых).
Я прямо удивлен, что и в статье и в комментариях нет ни слова про Apollo Client (хотя комментаторы упомянули GraphQL).
Да, это GraphQL и самые большие бонусы будут именно когда надо данные с сервером синхронизировать и сервер на GraphQL, но тем не менее — Apollo Client это же по сути и есть набор нормализованных сторов с подписками и кучей магии для упрощения работы...
https://www.apollographql.com/blog/the-future-of-state-management-dd410864cae2/ вот статья. В 3ей версии особый упор на работу без сервера. А учитывая, насколько несложно можно написать GraphQL обертку над REST — настоятельно рекомендую в эту сторону посмотреть.
Да, можно просто получить данные используя GraphQL query и положить результат в человеческое место (MobX) И не заниматься извращениями.
Использовать Apollo для стейт менеджмента это BDSM путь, но осуждать вас за это конечно я не буду, это ваше дело) Ведь этого же не будет на моих проектах) А такие проекты, где вот так пишут код и поддержка которых превращается в ад и гигантскую лапшу, переписывать с нуля я люблю)
Зря вы так. Чем MobX — человеческое место, а Apollo Cache нет? Нормализованный, удобный, с подписками. Это не стейт менеджмент, это хранилище объектов в первую очередь. Барахло из REST туда класть тоже весьма удобно. Вы перед укладкой в MobX данные нормализуете же? Чтоб при получении с сервера нового объекта везде юайчик обновился и вот это все?
Так почему бы не использовать сразу два инструмента? Объекты, нуждающиеся в синхронизации с сервером (REST/GraphQL не важно) — в Apollo, а клиентский стейт — в MobX. Только в таком случае правда велика вероятность, что все, что может MobX можно сделать на хуках и контексте...
Я вижу у вас проблемы с нормализацией данных, вы живете в прошлом мысля по Redux'овски и передавая в компоненты ID'шники и потом выцепляете из стейта объекты по айдишникам, так вот в человеческом мире мутабильности так делать не нужно.
Вы зря за меня додумываете. "мысля по Redux'овски и передавая в компоненты ID'шники и потом выцепляете из стейта объекты по айдишника" — это дичь и я не знаю, зачем это пропагандировалось…
Просто если вы вынимаете из сервера объекты, особенно со связями, то вам волей не волей придется их складывать в хранилище. Это не имеет, конечно, смысла для простого сайтика, но если появляются серверные подписки (хотя и без них тоже, после определенной сложности UI) — надо чтобы UI был всегда консистентен, то вы переизобретете Apollo через MobX так же как парень в презентации переизобрел MobX через Redux.
Добавлю, что помимо консистентных апдейтов, когда не важно откуда пришел объект (пуш или загрузился с очередным фетчем), допустим, с юзером, у которого онлайн статус сменился — этот статус обновится на всем сайте везде, где видно, Apollo еще умеет оптимистичные апдейты, пагинацию, батчинг без костылей, и много чего еще, без чего по-настоящему большое приложение вы не построите.
Для синхронизации состояний клиента и сервера я лучше тула не видел. Чтоб вот взять и пользоваться без доделок.
Каждому интсрументу свое применение. Не стоит возводить какой-то один тул на пьедестал.
надо чтобы UI был всегда консистентен
И? В чем проблема то? Причем тут Apollo или GraphQL?
допустим, с юзером, у которого онлайн статус сменился — этот статус обновится на всем сайте везде, где видно
И? В чем проблема то? Причем тут Apollo или GraphQL?
пагинацию, батчинг без костылей, много чего еще, без чего по-настоящему большое приложение вы не построите.
пагинацию — С каких пор пагинация это проблема и выделяется в виде какой-то особенности? Это же элементарщина.
батчинг без костылей — так если вам по кайфу заворачивать всё в action/runInAction то пожалуйста, не используйте setTimeout'ный костыль.
много чего еще, без чего по-настоящему большое приложение вы не построите. — серьезно?
И? В чем проблема то? Причем тут Apollo или GraphQL?
При том что он под это заточен, т.к. это хранилище структурированных связанных данных, а не просто клиентский стейт (который туда тоже можно положить, но я не только про это).
батчинг без костылей
я и про запросы на сервер, а не только батчинг клиентских апдейтов, там все батчится
Это же элементарщина
А если добавить в уравнение наличие БД на клиенте для оффлайн режима? А если пагинация бесконечная и по миллионам записей, с прыжками в любое место? И сначала данные надо забирать из одного места, и подтягивать из другого. Data retention писать надо...
большое приложение вы не построите. — серьезно?
Ну, попробуйте построить приложение размером со Slack (и с функциональностью как в Slack), и вы поймете. У меня такое в наличии есть, 50 разработчиков обслуживают. На MobX написано. И не дураки писали. Но проблемы есть в любом туле.
Вы меня не совсем слышите, похоже. Я говорю про то, что Apollo это, конечно, в первую очередь решение для работы с данными с сервера и локальными. И туда, в принципе, можно добавить и часть клиентского стейта, он переварит. Мой посыл в том, что не надо тащить MobX куда ни попадя, местами и редакс и плейн-ванилла и хуки справятся ничуть не хуже. Дифференцированный подход надо применять.
Никакой MobX, Redux, Apollo и т.д. и т.п. никогда не спасет от кривых рук. Не надо кривые руки прикрывать виной библиотеки.
Не умеешь ей пользоваться? Не берись. Вот и всё, я видел проекты где есть MobX, но весь там код настолько убогий, что плакать хочется и что теперь? Это вина MobX и React? Кривые ручки тут не при делах?
Всё же стоит признать, что пряморукие люди — это скорее исключение, чем правило, поэтому хороший инструмент должен направлять даже кривые руки в правильное русло.
Соглашусь. В командах часто смешанный состав, есть мидлы, есть синьоры, есть новопришедшие, которые в глаза проект не видели, есть текучка, и когда набирается человек 50 — выбор инструмента критически важен, т.к. если выбор верный, люди будут быстрее вникать и меньше создавать случайных проблем.
Плюс к этому есть бизнес, агрессивный и с конкурентами, когда надо делать "вчера", требования меняются и архитектура, которая вчера была прекрасна и могла бы прослужить долго, оказывается устаревшей. Вы пандемию планировали, например? А что если компании надо существенно сменить направление? Практика показывает, что даже очень умные люди, могущие писать код и умеющие в архитектуру и рефакторинг, вынуждены идти на компромиссы под прессингом реальности. И хорошие инструменты помогают в этом.
Я не теоретизирую, а сужу по практике — у нас в компании два продукта: грубо говоря аналоги Slack и Zoom. Там и там по много людей. Один на MobX, другой на Redux. Оба весьма неплохо справляются со своими задачами. У обоих есть сложности.
Не нужно всех подряд винить в кривых руках. Инструмент надо выбирать так, чтоб плыть в потоке, а не бороться с ним на каждом шагу.
Если для поддержки приложения размеров Slack требуется 50 человек, то инструмент явно выбран не правильно. Тут и 5 много при нормальных технологиях.
Это 50 китайцев и там как раз MobX. Вот и верь в совпадения — вы в точности угадали к чему были мои доводы ;) справедливости ради, там часть многостаночники, и тесты пилят и инфраструктуру, но людей реально много задействовано. И темпы разработки высокие. Да и сам Slack не так уж мал, 5ю точно не обойтись, там много фич, которые не сразу видны.
В командах часто смешанный состав, есть мидлы, есть синьоры, есть новопришедшие, которые в глаза проект не видели, есть текучка, и когда набирается человек 50 — выбор инструмента критически важен, т.к. если выбор верный, люди будут быстрее вникать и меньше создавать случайных проблем.Я согласен, но видел выступление, где чувак отнес к минусам Mobx сложность расширения команды, т.к. на рынке почти нет разработчиков с опытом Mobx.
Меня это удивило. Это же насколько в ИТ не доверяют навыкам и обучаемости других. Mobx от силы 3 дня изучать. Плюс в их команде все умели с ним работать и могли помочь быстрее вникнуть. То есть берешь каждого третьего разработчика с опытом react или хотя бы с опытом vue и не тратишь время на поиски.
Я бы понял, если бы у них самих не было опыта с Mobx или они не знали как более-менее нормально самостоятельно спроектировать приложение.
Я бы понял, если бы такая ситуация с Redux возникла. Его действительно изучать около месяца, плюс еще пару лет не будет понимания, зачем всю эту кучу кода писать.
в ИТ не доверяют навыкам и обучаемости других
Нельзя сказать что у них на этот совсем нет оснований. Большинство "senior react developer"-ов не знают не только основ React, но и основ JS. Про основы computer science вообще молчу. Это уже само по себе хорошо демонстрирует способности и\или желания к обучаемости.
И teamlead думает, если эти даже свой инструмент не знают, сколько лет им потребуется чтобы осилить наш инструмент… Не придётся ли за ними чуть ли не каждый MR самому писать.
Я вот со временем забываю некоторые основы чего угодно, если годами не приходится использовать или если редко нужно. Или я должен на протяжении всей жизни помнить все, с чем когда-либо сталкивался? Или постоянно повторять старое вместо изучения нового?
К тому же, основы — понятие растяжимое, у каждого список основ будет отличаться, т.к. опыт как обучения, так и работы разный.
Отправить этого teamlead-а прособеседоваться в несколько разных контор и он много где будет отнесен к группе:
'Большинство «senior react developer»-ов не знают не только основ React, но и основ JS.'
Почему так бывает? Да потому, что программистов на собеседованиях оценивают по тому, чем они на работе не занимаются — как они умеют рассказывать про те вещи, которыми пользовались когда-либо. Кто не волнуется на экзаменах и у кого язык без костей, те справляются, остальные успешно устраиваются только после нескольких десятков попыток в разные конторы, т.к. на работе делают задачи, а не рассказывают про то, как их делают.
Сразу скажу, я не знаю как лучше провести собес. Сам наверное давал бы несколько разных не сложных кусков кода, много чего затрагивающих, и попросил бы рассказать, что там делается, что используется, что плохо в коде сделано и как можно улучшить.
Я хоть и не люблю поделки этого всем известного товарища, но он правильно пишет, что каким бы разработчик не был крутым, у него есть пробелы в знаниях. Каждый разработчик в чем-то он хорош, в чем то плох. У каждого свой опыт, свои интересы.
overreacted.io/ru/things-i-dont-know-as-of-2018
Ни одного такого сеньора не встречал
А вы проводите собеседования? Если да, то к вам правда приходят другие кандидаты?
К тому же, основы — понятие растяжимое, у каждого список основ будет отличаться
Ну судите сами. Задаём вопросы вида:
const a = { b: 1 };
a = { b: 2 };
console.log(a); // ?
a.b = 2;
console.log(a); // ?
или такие:
const a = [{ a: 1 }];
const b = [...a, { b: 2 }];
b[0].a = 2;
console.log(a, b);
Если человек отвечает не правильно — ничего страшного. Мы же не роботы, и я понимаю — нервы. Даю подсказки.
Или это тоже слишком сложно?
Ваши вопросы не сложные, они на внимательность. Половина ответят не правильно, потому что в первом примере упустят из виду, что const, а не let, а во втором, потому что упустят, что в b копируется ссылка на объект { a: 1 } и «a» станет хранить [{ a: 2 }]. С первого раза или с третьего правильно ответят, разницы в знаниях это не покажет.
Половина ответят не правильно, потому что в первом примере упустят из виду
не-не-не, я же написал:
Если человек отвечает не правильно — ничего страшного. Мы же не роботы, и я понимаю — нервы. Даю подсказки.
Очень многие не отвечают ПОСЛЕ подсказок. Дедуктивным методом выясняется что люди реально НЕ ЗНАЮТ этих вещей (и многих других, к примеру, мало кто умеет в простейшие замыкания).
С первого раза или с третьего правильно ответят, разницы в знаниях это не покажет
Повторюсь. Они и с 2000 раза правильно не отвечают. Я не тороплю и помогаю, задаю наводящие вопросы, привожу примеры...
Они не знают как работает, к примеру, деструктуризация. Для них это магия "под капотом V8". Я не шучу. Регулярно слышу от них "ну они там у себя всё оптимизировали". Одним вопросом за другим я подвожу человека к тому, чтобы он хотя бы сам прикинул по какому принципу может работать ...
. Бог с ним, что человек не знает ни про какие итераторы, Symbol-ы и прочее, но… Они реально верят что это копирование в глубину.
И это касается почти всех аспектов языка. Шаг влево, шаг вправо от "копипасты" с SO, и человек присел. Я уж молчу про то, что 99% кандидатов не морщатся от .indexOf внутри .filter (O(n^2)), т.к. всё что связано с алгоритмами это прямо сразу конец света. Ребята валятся на уровне примитивов JS-а.
С TS не лучше. Почти все тестовые задания на TS просто завалены any. Некоторые просто пишут JS код в TS файлах.
Может фуллстеки часто приходят? Они обычно больше по бэку и зачастую JS вообще не учат и разбирались с ним немного по ходу работы. Особенно, если они работают со времен jQuery.
С TS не лучше. Почти все тестовые задания на TS просто завалены any. Некоторые просто пишут JS код в TS файлах.TS пока не везде используется. У некоторых просто нет возможности использовать его на рабочих проектах. Неудивительно, что фигню пишут, если впервые его используют. Особенно, если со строгой типизацией не работали. Но писать JS в TS файлах и вправду перебор.
В целом, с редуксом возникает ощущение, что пользы от него меньше, чем новых проблем. MobX же, напротив, оставил приятное впечатление.
Каждый раз после очередного собеса, подумываю:
"А может пересилить себя и перейти на Redux, Redux-Toolkit, Redux-Saga, Redux-Thunk?"
Потом натыкаюсь на подобные статьи и становится спокойнее на душе.
Спасибо!
Почему мы выбрали MobX, а не Redux, и как его использовать эффективнее