Автор: Маслов Андрей, Front-end разработчик.
Время чтения: ~10 минут
Business logic with ease.
Содержание:
О статье.
Почему нужно использовать effector ?
Концепция.
Полезное и основное из api.
Как работает ядро Effector, простым языком.
Итоги.
Полезные материалы.
О статье
Этой статьей я открываю туториал из трех последующих статей, посвященных Effector JS - не только удобному менеджеру состояний, но и мощнейшему из инструментов на сегодняшний день (по-моему личному мнению).
Часть №1 будет нести ознакомительный характер c инструментом, чтобы вы могли понять, нужен ли вам Effector или нет. Разберем основные возможности и затронем то, как работает ядро библиотеки.
Почему нужно использовать Effector ?
Больше никакого бойлерплейт-кода.
Приложение принимает изначально расширяемую архитектуру (т.к эффектор позволяет изолировать бизнес-логику по процессам, здесь же стоит сказать и об feature-sliced архитектуре, о которой мы поговорим в последующих статьях).
Удобное и большое API, которое избавит разработчика от многих рутинных вещей.
Бизнес-логика теперь не "размазана" по файлам-контроллерам, а изолирована по процессам, интерфейсы не пересекаются с логикой (подобие реализации MV* паттернов).
Никакой магии, все построено на графах и подписках (об этом поговорим в конце этой статьи).
Есть русскоязычное комьюнити, подробная документация на русском и английском языках.
Постоянная поддержка, релизы с фиксами и новыми фичами.
Легковесность и скорость.
Поддержка TypeScript.
Концепция
Работу всего стейт-менеджера обеспечивает три основных юнита:
Store
Объект для хранения данных.
createStore - функция создания стора, название принято начинать со знака $.
Event
Этот юнит является главенствующей управляющей сущностью. С помощью event запускаются реактивные вычисления в приложении.
createEvent - функция создания события.
Вы можете подписать ваш store на какие-либо эвенты, при которых все зависимые от этого стора компоненты будут обновляться, сделать это можно при помощи .on, передав в метод первым аргументом - юнит, вторым - коллбэк функцию, которая будет возвращать результат изменения стора.
Пример:
//init.js
export const eventPlus = createEvent()
export const eventMinus = createEvent()
export const $storeCounter = createStore(0)
.on(eventPlus, (store) => store + 1)
.on(eventMinus, (store) => store - 1)
//components.jsx
export Component = () => {
const count = useStore($storeCounter)
//От этого хука можно будет отказаться,
//при использовании effector/reflect (рассмотрим в последующих туториалах)
return (
<h1>{ count }</h1>
)
}
Согласитесь, выглядит очень лаконично и просто.
Effect
Этот юнит очень похож на предыдущий, за тем лишь исключением, что эффект создает цепочку событийных вызовов, состоящих по факту из event. Обычно этот юнит используется при работе с асинхронными функциями.
createEffect - функция создания эффекта, принимает в себя первым аргументов коллбэк функцию - обработчик вызова эффекта, название такой функции принято заканчивать на Fx.
Пример:
//api.js
export const getCount = (payload) => {
return axios.get('/count', payload)
}
//init.js
export const getCountFx = createEffect(getCount)
Effect предоставляет множество эвентов, например, doneData, failData, pending и тд. (Подробнее можно ознакомиться в документации).
Ниже приведу пример работы с эффектом и его возможности, сразу стоит сказать, что работа с variant из effector/reflect облегчит обработку состояний компонента, но как ранее упоминал, разберем в следующих статьях.
//init.js
export const $count = createStore(0)
.on(getCountFx.doneData, (_store, res) => res.data.count)
//components.jsx
export Component = () => {
const count = useStore($count)
if (getCountFx.pending) {
return <h1>Loading...</h1>
}
if (getCountFx.failData) {
return <h1>Error</h1>
}
return (
<h1>{ count }</h1>
)
}
Полезное и основное из api
combine - позволяет комбинировать несколько сторов и создавать один производный.
Создадим стор, который будет хранить булево значение дизейбла кнопки submit, если запрос на получение счетчика в статусе "pending" или если этот запрос завершился в блоке catch. Третий аргумент является необязательным и служит для трансформации состояния.
const $submitDisabled = combine(
getCountFx.pending,
getCountFx.failData,
(pending, faildData) => pending || faildData
)
forward - создает связь между юнитами, с которыми мы разобрались чуть выше.
Напишем код, который будет выводить ошибку.
Forward принимает объект с двумя полями: from и to, которые ожидают юниты (или массивы юнитов), при выполнении from вызовется юнит to.
const showErrorFx = createEffect(() => showToast('Something went wrong'))
forward({
from: getCountFx.failData,
to: showErrorFx
})
guard - метод, который позволяет запускать юниты по условию.
Напишем код, который будет отправлять запрос формы на бэкенд, если форма валидна.
guard({
clock: sendEvent, //юнит, при срабатывании которого будет выполняться filter
filter: $isValid, //дальнейший вызов target возможен при filter = true
source: $form, //данные, которые будут передаваться в target
target: submitFormFx // юнит, который будет вызван при вызове clock и истинном значении filter
})
sample - метод, принцип работы как у guard, добавляется аргумент fn - коллбэк, результат вызова которого будет передан в target.
sample({ source?, clock?, filter?, fn?, target?})
API Effector предоставляет большое разнообразие методов, выше вы видите лишь те, которые я использовал на проекте чаще остальных, и это малость из доступных, обязательно рассмотрите следующие методы: is, restore, split, attach. Так же почитайте про нерассмотренный ранее юнит domain.
Просто о том, как работает ядро Effector.
Основа - обход графа в ширину, где вершины графа являются событиями в очереди, которые хранятся в объекте ядра, выглядит так:
export type Node = {
id: ID
next: Array<Node>
seq: Array<Cmd>
scope: {[key: string]: any}
meta: {[tag: string]: any}
family: {
type: 'regular' | 'crosslink' | 'domainn'
links: Node[]
owners: Node[]
}
}
Рассмотрим три основных свойства объекта:
next - массив ребер графа (ссылки на следующие вершины)
seq - массив с последовательностью шагов
scope - объект данных, необходимый для работы шагов
При попадании данных в ядро (например, вызов event) - запускается цепочка событий, которая обновляет все связанные сторы, и собственно компоненты. При попадании ноды в очередь, у нас происходит выполнение шагов из seq (вычисления, фильтрация, перемещение ноды в режим ожидания, и другие, в материалах оставлю ссылку для полного ознакомления)
К слову, это упрощенная модель работы эффектора. Так, например, очередей в ядре 5, а не 1. Все эти очереди отличаются приоритетом к выполнению.
Итоги
При первом использовании данной библиотеки у меня было двоякое чувство, ведь зачем мне отказываться от того же Redux или MobX, "и так все хорошо", лишь после полугода плотного использования effector на проекте я стал замечать, что разрабатывать стало проще и быстрее, а код стал структурированным и понятным.
Хоть на просторе интернета и мало информации о разных решениях, чтобы новичку было легче вкатиться, все это перекрывается прекрасным комьюнити, которое всегда готово прийти на помощь, например, в tg канале. А этим циклом статей я попытаюсь сделать процесс погружения быстрым и легким.
Не стоит в комментариях заниматься холиварами, не воспринимайте статью, как единственный и истинный источник правды. Оставляйте комментарии по непоняткам, найдем правду вместе :D
В этой статье мы разобрали лишь мизерную часть того, что умеет effector, так что прошу пробежаться еще по материалам для закрепления.
Далее мы развернем реальное приложение в связке TypeScript + Effector (в документации все примеры на js, поэтому для новичков использование ts может стать не самым приятным делом), поговорим о feature-sliced архитектуре и best practices.
Материалы для закрепления:
Комментарии (74)
nin-jin
11.11.2022 19:24+1Легковесность и скорость.
https://github.com/artalar/reactive-computed-bench
Больше никакого бойлерплейт-кода.
//api.js export const getCount = (payload) => { return axios.get('/count', payload) } //init.js export const getCountFx = createEffect(getCount) export const $count = createStore(0) .on(getCountFx.doneData, (_store, res) => res.data.count) //components.jsx export Component = () => { const count = useStore($count) if (getCountFx.pending) { return <h1>Loading...</h1> } if (getCountFx.failData) { return <h1>Error</h1> } return ( <h1>{ count }</h1> ) }
vs
//api.js export class API extends Object { @mem count() { return fetchJSON( '/count' ).data.count } } //counter.jsx export class Counter extends Component< Counter > { count(){ return API.count() } vdom() { return <h1>{ this.count() }</h1> } }
все это перекрывается прекрасным комьюнити, которое всегда готово прийти на помощь, например, в tg канале
Или забанить, если будешь задавать глупые вопросы.
amas1ov Автор
11.11.2022 22:19Больше никакого бойлерплейт-кода.
Немного недопонимание вышло, возможно.
Речь шла о бойлерплейт коде именно стейт-менеджера, например:
Redux//actions.ts - Определить экшен const someAction = function (props) { return { type: "SOME", props } }; //reducer.ts - обработать экшен и изменить стор const reducer = function(state, action) { switch (action.type) { case "SOME": return ... } return state; } //Component.tsx const Component = () => { const dispatch = useDispatch() const handleSome = () => {dispatch(someAction())} return (...) }
vs Effector
//init.ts const someAction = createEvent() const $someStore = createStore() .on(someAction, (state) => state) const Component = () => { const handleSome = () => {someAction} return (...) }
И это мы в редасе еще стор сам не создавали, initialState не описывали и тд )
Думаю, вы меня поняли.
Про бан ничего сказать не могу, очень грустно, если это было просто за глупый вопрос.
Спасибо за комментарий.nin-jin
12.11.2022 05:49+1Я вам привёл эквивалентный код, который действительно без бойлерплейта. А вы сравниваете один боилерплейт с другим.
funca
11.11.2022 22:53+1Больше никакого бойлерплейт-кода.
Смотрю в код тестов по вашей ссылке
Effector
const entry = createEvent<number>() const a = createStore(0).on(entry, (state, v) => v) const b = a.map((a) => a + 1) const c = a.map((a) => a + 1) const d = combine(b, c, (b, c) => b + c) const e = d.map((d) => d + 1) const f = combine(d, e, (d, e) => d + e) const g = combine(d, e, (d, e) => d + e) const h = combine(f, g, (h1, h2) => h1 + h2)
mol
const entry = new $mol_wire_atom('entry', (next: number = 0) => next) const a = new $mol_wire_atom('mA', () => entry.sync()) const b = new $mol_wire_atom('mB', () => a.sync() + 1) const c = new $mol_wire_atom('mC', () => a.sync() + 1) const d = new $mol_wire_atom('mD', () => b.sync() + c.sync()) const e = new $mol_wire_atom('mE', () => d.sync() + 1) const f = new $mol_wire_atom('mF', () => d.sync() + e.sync()) const g = new $mol_wire_atom('mG', () => d.sync() + e.sync()) const h = new $mol_wire_atom('mH', () => f.sync() + g.sync())
Чисто эстетически первое выглядит лучше - сразу вспомнился курс теорката. Второе больше напоминает какой-то ассемблер.
nin-jin
12.11.2022 07:19Если что, на $mol так не пишут. $mol_wire_atom - низкоуровневая абстракция, эквивалентная остальным решениям в этом бенчмарке. Типовой код на $mol_wire можно глянуть, например, тут.
funca
12.11.2022 10:09Если что, на $mol так не пишут.
Ну и какая польза от такого бенчмарка? Если оторванные от реальности способы допустимы, то нарисовать линию на графике можно гораздо проще.
Кстати, а как там должно быть написано, чтобы это стало похоже на правду?
nin-jin
12.11.2022 12:04+1Да никакой. Бенчмарк этот довольно далёк от реальности. Там замеряется кейс вида "есть сложный DAG с тривиальными формулами, и на каждый чих они все пересчитываются". Это худший случай для $mol_wire и MobX и лучший для Effector и S.js. Но даже в этом случае, эффектору не удаётся конкурировать, в отличие от S.js. На более-менее реалистичном кейсе с кучей данных, тяжёлыми вычислениями и динамическими зависимостями эффектор просто встанет колом.
Вот, кстати, более свежие результаты на моей машинке:
funca
12.11.2022 12:38Интересно какие результаты для $mol_wire_solo (насколько я понимаю это идиоматическая реализация как на практике). В последнем коммите этот код закомментирован.
nin-jin
12.11.2022 13:40+2Он закомментирован так как даёт наводки в этом бенчмарке (взаимный прогрев одной библиотеки) и делает больше, чем нужно для этого бенчмарка. Но если их поменять местами будет:
funca
13.11.2022 00:39Спасибо, теперь выглядит понятнее.
взаимный прогрев одной библиотеки
Согласен. Было бы удобнее если бы тесты для разных библиотек находились в разных модулях и запускались независимо друг от друга.
nin-jin
13.11.2022 06:32-2Там два разных API к одной библиотеке. Использование одного прогревает и другой. Аналогичная проблема есть и при использовании общей зависимости разными библиотеками. Поэтому адекватно замерить непрогретую библиотеку можно разве что перезапуская ноду каждый раз. Хотя, смысла замерять холодные либы не очень много. Значение имеет обычно горячий код.
kai3341
12.11.2022 16:04+2Не могли бы вы объяснить, каких попугаев меряет бенчмарк по оси Y? От дополнительной информации касательно оси X тоже не откажусь
markelov69
12.11.2022 16:13+1Это бестолковые "бенчмарки", не относящиеся к реальным проектам и реальной логики приложений. Цикл "for in" быстрее чем цикл "for of", но в реальных клиентских вэб приложениях вы никогда не заметите экономии в микросекнудах и наносекундах. Тут нужно опираться только на то, какой код вам позволяет писать та или иная библиотека. Это важнее на порядке, чем то, на сколько надо секунд X обгонит Y. Тем более речь о клиентском приложении, а не о сервере.
noodles
12.11.2022 12:17+3лишь после полугода плотного использования effector на проекте я стал замечать, что разрабатывать стало проще и быстрее
Т.е. судя по всему, первые полгода было не проще и не быстрее. А потом подпривыкли, освоились в проекте, научились в "effector" - и дело пошло. Вопрос в том, что следующему разработчику надо будет теже полгода + время на вкуривание уже написанного кода, и только потом дело пойдёт.
Всё-таки выборка для оценки должна быть побольше, например три проекта на мобх, и три проекта на эффекторе. При чём не так, что начинал с нуля, а потом уволился (потому что говорят что надо "развиваться" и прыгать с места на место). А наоборот, чтоб пришёл на проекты уже которые в возрасте.
Мне достался двухлетний проект на mobx-е, он большой (при чём не вплане формочек, там только одна форма на входе, а вплане накрученной логики, там и sip + web-rtc, и electron). Всё супер понятно, уже четвёртый год пошёл проекту, а он развивается быстрее всех, с той же скростью что и была в начале, при этом без увеличения энтропии и wtf-индекса.. вся сложность упразднена. Порой такие требования прилетают, что кажется если бы не mobx - проект бы уже переписывали или закрыли. И самое классное - ни одного простигосподи usecallback-а, useMemo, memo в реакте. И только один реактовский контекст на всё приложение. Реакт чисто как шаблонизатор. Вобщем очень удобно.markelov69
12.11.2022 13:56+1Мне достался двухлетний проект на mobx-е, он большой (при чём не вплане формочек, там только одна форма на входе, а вплане накрученной логики, там и sip + web-rtc, и electron). Всё супер понятно, уже четвёртый год пошёл проекту, а он развивается быстрее всех, с той же скростью что и была в начале
Всё правильно, так и должно быть, вот что бывает когда адекватные люди разрабатывают и во главе всего стоит "KISS (Keep it simple, stupid), YAGNI (You aren't gonna need it), Чем меньше, тем лучше (Less is more)".
Порой такие требования прилетают, что кажется если бы не mobx - проект бы уже переписывали или закрыли
Так и случается в 100% случаев)
amas1ov Автор
12.11.2022 14:02+1Все от сложности проекта зависит, верно подметили.
Вопрос в том, что следующему разработчику надо будет теже полгода + время на вкуривание уже написанного кода, и только потом дело пойдёт.
Если рядом будет разработчик, который уже умеет в эффектор, то время на погружение сократится многократно, а одному, действительно, будет сложновасто.
Всё-таки выборка для оценки должна быть побольше, например три проекта на мобх, и три проекта на эффекторе. При чём не так, что начинал с нуля, а потом уволился (потому что говорят что надо "развиваться" и прыгать с места на место). А наоборот, чтоб пришёл на проекты уже которые в возрасте.
Соглашусь.
У меня с МобХ первый проект сейчас, поэтому никакой конкретики не приводил, плевки в сторону других решений не делал. Вкатился быстро, да так же как и можно вкатиться в проект на эффекторе (если рядом тебя за ухо поднатаскают), единственное отличие - у эффектора инструментарий будет пошире (и в ssr и в байндинг пропсов (никаких тебе вызовов юзстора в компонентах), ну и тд, следующую статью по инструменталу как раз готовлю), в этом и сложность для многих.
Спасибо за комментарий )DmitryKazakov8
12.11.2022 14:28+2Немного вклинюсь по поводу юзстора - решается оберткой (я пишу на классовых компонентах, поэтому пример для них)
import { observer } from 'mobx-react'; import { Component } from 'react'; import { TypeGlobals } from 'models'; import { StoreContext } from './StoreContext'; export class ConnectedComponent<TProps = any, TRoute = never> extends Component<TProps, any> { // SSR not able to recalculate components on observable updates, // so on server side it has to be rendered twice static observer = IS_CLIENT ? observer : (someComponent: any) => someComponent; // Describe context, so no boilerplate in components needed static context: TypeGlobals; static contextType = StoreContext; declare context: TypeGlobals; } // Any component class AnyComponent extends ConnectedComponent<TypeProps> { render() { this.context // Perfectly typed and reactive, no boilerplate at all this.props // Perfectly typed and reactive, no boilerplate at all } }
В Webpack делаю транформер, который все компоненты, которые экстендят ConnectedComponent превращаются в
const AnyComponent = ConnectedComponent.observer(class AnyComponent...)
Так получается реактивность без сахарных декораторов (чтобы использовать не Babel, а более шустрый SWC транспайлер), с отдельной логикой для SSR и без бойлерплейта.
К оригинальному комменту - тоже один контекст на все, это супер-эффективно, в отличие от DI разрозненных сторов для "изоляции". Когда нужно - расширяешь глобальный, когда не нужно - очищаешь.
nin-jin
13.11.2022 07:57-2Итак, продолжаем нытьё и хамство..
Effector JS - не только удобному менеджеру состояний, но и мощнейшему из инструментов на сегодняшний день
Удобное и большое API, которое избавит разработчика от многих рутинных вещей.
все это перекрывается прекрасным комьюнити, которое всегда готово прийти на помощь
markelov69
По вашему вот этот вырви глаз "код" и "подход", кодом этого конечно можно с большой натяжкой называть. Хоть на грамм может что-то сопоставить MobX'у? Да никогда, между ними пропасть в триллионы световых лет. Даже убогий Redux на фоне Effector выглядит гораздо лучше, т.к. там все гораздо очевидней.
Если ваша цель запороть проект с самого начала, то да, возьмите Effector, пусть у всех взорвется мозг и будет взрываться дальше в геометрической прогрессии с добавлением каждой новой фичи и с каждым расширением функционала. С таким же успехом вы можете взять RxJS, результат будет одинаковым и НЕжизнеспособным.
amas1ov Автор
В целом, вы правы.
Все же инструмент должен выбираться под конкретные задачи и под конкретную команду.
На каждое возражение можно найти контраргумент.
У MobX (сейчас как раз на проекте используем) есть свои проблемы (как и у Effector, не пытаюсь боготворить). Например, неявные подписки на наблюдаемые изменения, из-за Proxy приходится писать постоянно модели данных (иначе консоль у тебя засыпается предупреждениями, и это не говоря про реактивный контекст). Store становится сборной солянкой из наблюдаемых, вычисляемых значений, экшенов и всего остального, если фича начинает разрастаться.
В эффекторе же у вас есть только события, которые способны изменять стор.
Effector будет полегче того же МобХ почти в 2,5 раза (говорю про связку с реактом).
Подход, действительно, необычный. Я упоминал в статье, что первые полгода не мог привыкнуть. Про "вырви глаз код", здесь каждому свое, по мне так код effector один из наиболее приятных.
DmitryKazakov8
Не могли бы привести пример предупреждений в консоли? У меня нету ни в одном проекте, как вызвать?
"Неявные подписки" - это отличная практика вынесения сложности за рамки основного кода. То есть работаешь по виду с обычными объектами, а на изменения определенных параметров, в том числе глубоко вложенных, можно еще и подписываться, это же мечта из 2000-х. Я тогда море ухищрений делал, чтобы добиться реактивного рендеринга, теперь же все очень просто.
Про стор, в котором все намешано - это конечно антипаттерн, но никто не заставляет его так организовывать. Особенно это касается экшенов (функций изменения стора), намного эффективнее их выносить, чтобы они имели доступ к разным сторам. Поэтому нельзя отнести это отнести в минусы инструмента, который делает объекты реактивными и дает полную свободу в реализации.
"В эффекторе же у вас есть только события, которые способны изменять стор" - ну и в Mobx есть функции-экшены, которые изменяют стор, только без необходимости иммутабельно писать как в редаксе (store, data) = > ({...store, items: store.items.map(item => ({...item, isViewed: data.id === item.id}))}) , разводя дубляж и значительно усложняя код, и заводя прослойку в виде событий, которые являются чистым бесполезным бойлерплейтом. Все, что нужно - вызвать функцию, она меняет нужный параметр в сторе, автоматически происходит эффективный перерендеринг.
Насчет "полегче" - аргумент, конечно, хороший, если измерения проводить в маленьких проектах, где размер библиотек намного превышает размер бизнес-логики, но уже в средних размер приложений с mobx за счет практически отсутствия бойлерплейта уже начинает выравниваться, в крупных - будет меньше.
inoyakaigor
Либо вы выключили предупреждения о прямой записи в наблюдаемые значения либо у вас предыдущая мажорная версия Мобикса. Последняя версия по-умолчанию спамит варнингами если вы без экшена пишите в наблюдаемое значение
markelov69
Разумеется нужно делать так.
inoyakaigor
Вам видимо никогда не приходилось искать кто изменил какую-то переменную в сторе. Ставить костыли через itercept или того хуже spy. Иначе вы бы так не говорили))
markelov69
Ну в 99.99% случаях я знаю что делаю, когда пишу код) И знаю какая переменная когда и зачем должна изменятся и читаться, что при этом будет происходить и тп)
А вообще всё просто, щелкаешь на переменную. правой кнопкой мыши и:
WebStorm:
VS Code:
Там видно где переменная читается, где она мутируется. Если этого оказалось не достаточно для понимания того места, которое вы ищете, то опять же всё просто, на помощь приходит console.log. Весть процесс дебага в самом худшем сценарии занимает не более 5-10 минут. Но у меня например за 6 лет использования MobX приходилось не более 3-4 раз прибегать к такому дебагу.
Поэтому я считаю на все 100000% оправданно вырубать enforceActions чтобы не засирать код. По той же причине (не засирать код) я и runInAction(() => {}) не использую после асинхронных вызовов если мне надо смутировать более 1 реактивной переменной, ибо раньше у меня был автоматический батчинг настроен всегда, а начиная с react 18 и он стал не нужен, там и так рендер не синхронный стал.
DmitryKazakov8
Тоже пользуюсь Find Usages, дебаг очень простой. А чтобы не было ворнингов все эшены автоматом оборачиваются в action, а для батчинга runInAction, если после асинхронных вызовов. Я пока не считаю, что оправданно вырубать enforceActions - многое в коде зависит от батчинга (мне нужно чтобы несколько переменных менялись сразу, а не постепенно - не для рендера в Реакте, а для autorun, которые например отслеживают форму - надо чтобы и value поменялись, и isFocused, и валидаторы, и авторан вызвался 1 раз с финальными данными). И таких ситуаций много. Плюс я стараюсь делать библиотеко- и фреймворко-независимую архитектуру, а в альтернативах MobX тоже используются колбэки системной функции для батчинга, иначе его просто не выстроить синхронно.
К теме дебага - все экшены у меня еще логируются в визуальный интерфейс (история вызова + время на исполнение) в виде stack bars chart, что максимально сужает поиск по Find Usages.
markelov69
Ладно, вот бесплатный лайфхак для таких целей) Если конечно конфиг с автобатчингом вас не устраивает)
Работает как часики)
Тоже самое для reaction по аналогии.
Вот быстро проверить протестить - https://codesandbox.io/s/summer-leaf-hkv0x9?file=/src/index.js
DmitryKazakov8
Да, таких решений немало, но все они завязаны на setTimeout, я же выше писал про синхронные изменения - это тоже очень важно. Вот еще реализация, без подмены на кастомную функцию. Если вам не нужны синхронные подписки - хорошо, но у меня кодовая база именно на них завязана
markelov69
Спасибо что скинули мой же конфиг) Только у меня чуть более усложнен) И в нем учитывается нюанс c controlled input'ами.
Неа, не нужны, у меня всегда вся архитектура строится на том, что мы живем в асинхронном мире JS'a в браузере/на сервере)
Изменения и так синхронные) А вот потребность именно в мгновенных синхронных реакциях на них, ну не знаю не знаю) Всё таки всё вращается вокруг асинхронности у нас)
Но всё же приведите пожалуйста реальный боевой пример, где нужны именно синхронные autorun/reaction с забатченными изменениями, а асинхронные autorun/reaction с забатченными изменениями не подойдут, т.к. не будут работать для достижения ваших реальных боевых задач. Я просто вот думал думал и ничего в голову не может прийти, но расширять кругозор нужно всегда и иметь в виду дополнительные кейсы всегда полезно
DmitryKazakov8
Самое очевидное - SSR, реакции, которые навешиваются до этапа рендеринга должны отработать, чтобы рендер произошел с финальным состоянием. С асинхронщиной отрендерится с негидрируемым состоянием (хотя в примере выше первый колбэк вызывается синхронно, повторные - не сработают и повиснут в эвент лупе ноды, не влияя на рендер)
Цепочки авторанов (в первом меняется параметр, который слушает второй авторан, в третьем прослушивается результат второго) - хоть Реакт 18 и добавил асинхронности в рендер, я на это не полагаюсь, и на каждый рендер подготавливаю финальное состояние, чтобы отрендерилось 1 раз. Т.к. ориентируюсь на более-менее свободную замену частей приложения (в том числе рендерера типа Реакта), то важно синхронно подготовить данные для избежания ререндеров - это просто хороший тон и предотвращение проблем.
Ну и минорное замечание к решению выше - если подменять одну функцию другой и передавать в setTimeout(fn) то контекст теряется и в стеке вызовов будет batch вместо реального имени функции. Фиксится парой строк, но все равно об этом думать надо
Оказывается, это Mazaa шифруется под новым ником, интересно)
В реальных задачах я могу вешать 5 авторанов последовательно, и даже если меняется их порядок - то где-то работает некорректно. Конечно, это антипаттерн и нужны прямые руки и продумывать лучше, но как это всегда бывает - нужно все срочно и потоки МР по 5-10к строк ежедневные, вангую, что с асинхронным вызовом было бы проблем больше, чем порядок поменять.
markelov69
По пунктикам:
1) Вот именно что все отработает синхронно при первом вызове, а дальше чтобы не повисало тоже решается легко с помощью проверки на if (SSR_MODE) { return autorun(fn) } else { вариант с автобатчингом }
Так что аргумент в "против" не засчитан, т.к. с этой задачей справляется на ура)
2)
Цепочка авторанов влияющих друга на друга это антипаттерн) Можно загнать реакции в бесконечный цикл)
Ну и тоже относится очень сильно к вашему специфическому подходу) Абсолютно прекрасно любые задачи реализуются с асинхронными реакциями ан самом деле)
Ну это тоже не аргумент, на практике никто и никогда не меняет view с реакта на vue/angular/svelte, без конкретного переписывания, понятное дело что на peact можно сменить почти(!) безболезненно. И вы всё равно никуда не убежите от специфики работы с тем или иным view фреймворком. Так что тоже не засчитывается) Ну серьезно, кейсы из области фантастики или 1/1000000 в расчет никогда не берутся, иначе вообще никогда и ничто нам не подойдет.
3) Тоже из области фантастики и на практике разумеется не применимо, ибо тогда либо вы не понимаете что вообще делаете в этой жизни или дизайн вашего приложения слетел с катушек)
В общем для 99.99% проектов и людей, кроме вас предложенный мной вариант автобатчинга более чем подходит)) Хотя на самом деле и вам подойдет, т.к. единственный рабочий аргумент был про SSR, но и он элементарно контрится, поэтому оказался нерабочим аргументом)
DmitryKazakov8
Про "никто не меняет вью" в принципе согласен, но я скорее для спортивного интереса поддерживаю архитектурные части изолированными и абстрагированными. Не только Реакт могу поменять на Preact/Inferno/SolidJS и экспериментирую с заменой через адаптеры на другие рендереры (даже веб-компоненты), но и MobX на другие реактивные библиотеки, тот же Solid mutableStore. Это позволяет мне лично расти как специалисту и создавать долгоподдерживаемые проекты, заодно сравнивая перфоманс и набор фичей и оттачивая дизайн системы. И в целом стараюсь не завязываться на конкретные особенности (ex. версия определенной либы, которая рендерит асинхронно). И другие архитектурные части тоже делаю самодостаточными и независимыми. Поэтому хаки чисто для MobX в виде асинхронного автобатчинга не рассматриваю.
Конечно, можно и с ним жить, и утилиты подбить так, что будет и SSR, и нормальный стек вызовов, и соблюдена очередность реакций. Можно и на альтернативные библиотеки наворачивать такой функционал, но все равно мне ближе явный подход - где изменяется стор, оборачивать в batch (runInAction в mobx). Так добавляется бойлерплейт, но упрощается понимание за счет синхронности операций в и так непростом асинхронном мире JS, а в ряде случаев - улучшается перфоманс (незначительно, но в перегруженных компонентами страницах асинхронщина может привести к глитчам).
Забыл еще один пункт - роутинг у меня сейчас построен именно на синхронной реакции, асинхронная собьет определенную логику при уходе со страницы, но это недочет, надо будет добавить поддержку.
И еще - в асинхронной реакции может уже не быть той сущности, с которой она должна работать (компонент размаунтился, this = undefined, состояние модульного стора очистилось).
По существу особо сказать что-то против вашего подхода нечего, но к себе в проекты не потащу, т.к. вижу в этом не упрощение, а усложнение. Код без batch конечно становится лаконичнее, но добавляется асинхронный сайд-эффект с внутренней логикой, которые я не уважаю (в частности потому и хуки не использую).
markelov69
У меня тоже своя реализация роутинга с помощью MobX'a, react-router не уважаю, но там пофиг синхронно reaction отрабатывает или асинхронно) Ну суть понятна, у вас больше тягота к синхронности, а мне по большому счету не принципиально, но по умолчанию все под асинхронность проектирую и закладываю, наверное поэтому и проблем не знаю и ни с чем не борюсь)
mayorovp
А ничего, что ваш batchedAutorun выполняется ровно 2 раза?
Замените в вашем примере setTimeout на setInterval и посмотрите что происходит...
Заканчивайте уже херню советовать.
markelov69
Это знак, что лучше избегать autorun и использовать reaction)
А ещё лучше конечно использовать автобатчинг, не избегать асинхронность и не парится с засиранием кода.
Херю полнейшую советуете обычно вы, любитель засирать код и усложнять элементарные вещи на ровном месте, раздувать проблемы там, где им попросту нет и т.п. А тут я разок косячнул в примере и всё, нужно срочно самоутвердится и потявкать)
mayorovp
Или же можно просто передавать
{ delay: 1 }
в качестве options вместо нагораживания своих велосипедов...markelov69
Так не интересно, это слишком просто. А так да
did the trick
Ну в любом случае с автобатчингом таких проблем и заморочек не имеешь и отвыкаешь от всех этих неудобств
konclave
Досмотрел этот тред и после всех этих обсуждений типа "да тут надо подтюнить в конфигах и обернуть в все в функцию, которая будет пропатчивать что-то, чтобы ворнинги не сыпались" начальное утверждение "да mobx в тысячу раз проще" выглядит уже не таким однозначным =)
mayorovp
На самом деле там всё просто, главное — не начинать читать комментарии с "добрыми" советами
markelov69
Это просто были рассуждения над специфическими кейсами @DmitryKazakov8 из-за его сильной любви к максимально возможной синхронности))
Для всех остальных(в том числе меня) всё предельно просто:
И готово.
Это если вы не утопаете в легаси и используете React 18, а если ниже версия, то для input'ов реакция должна отрабатывать синхронно. Это тоже легко решается, просто за счет расширения React.createElement. Если актуально могу целиковое решения для React младше 18 версии приложить.
markelov69
Нет ни одной задачи во фронтенде, где React + MobX может не справится или может не подойти. За исключением тех, где нужно сэкономить каждую наносекнду и каждый байт памяти. Но тут в вэбе не разгуляться, только голый JS.
Тоже не совсем верно, т.к. если вашу вкусы скажем так специфичны в текущий момент времени, и потом вы увольняетесь/уходите на другой проект, то потом ваши так сказать вкусы должен будет расхлебывать кто-то другой. Есть большая проблема, одно дело код написан по простому, вот прям реально все просто, все читается слева направо и сверху вниз, заходишь в любой обработчик события и в нем же описано, что откуда мы взяли, куда что записали, куда какие запросы послали. Так вот, когда код написан так, то проблем вообще никогда не возникает, а вот когда вы начинаете накручивать лапшекод, запутываете элементарные вещи, притаскиваете всевозможный мусор типо effcotr'a, rxjs, xstate и т.д и т.п., то проект становится не жизнеспособным и способен хоть как-то худо бедно держаться на плаву, пока вы его поддерживаете.
Подписки явные. Просто они автоматические и работают вот время чтения реактивные переменных, там же getters/setters. Если вы этого не знаете или не понимаете, это сугубо ваши проблемы и вашей компетенции, так что этот жирненный плюс нельзя относить к минусам.
Вообще не понимаю о чем речь, просто набор слов. Уверен что вы что-то имели ввиду, но тут опять же без доли сомнения проблема только в ручках тех, кто пишет код.
Стор ровно такой, каким вы его делаете. Не больше, не меньше. Вы можете делать его большим, можете делать его маленьким, можете раздраконить его на сотни маленьких, вообще любой вариант. Опять же это жирнейший плюс, но из-за нехватки компетенций и не понимаю что и как нужно делать, вы записываете это почему-то в минусы.
В эффекторе мы получаем максимально убогий, нечитаемый, неочевидный, лапшеобрзаный, неподдерживаемый, write-only код, нет спасибо.
Очень смешно когда в качестве "аргумента" ставят десяток килобайт в бандле, который капля в море в реальных проектах. Ещё больше смешно, когда не называют конкретные цифры, а говорят в 2.5 раза, как бы приувиличивая значимость. Более того, на самом деле то, что в MobX кода минимально возможное количество, то опять же на реальном проекте эта ничтожная разница компенсируется и уходит в профицит.
Пол года Крал, пол года. С MobX открываешь документацию, потом 3.5 примера и все. через 3 часа ты как рыба в воде. О чем это говорит? Правильно, о том, что там все супер просто, понятно и наглядно. Изменил переменную, компоненты которые ее читают перерендерились, точка. Финита ля комедия.
Оно и понятно собственно)
Вот пример убогого и никчемного кода с MobX:
Это же вообще максимально не наглядный, не очевидный, не лаконичный код, а вообще полное дно и ибо тут столько проблем, что прямо тьма.
Только дело ваша любимая лапша из цепочек функций, ммм крастота то какая, ляпота. А $ в названиях переменных так это вообще вишенка на торте. Понимаю, понимаю)
funca
Добавьте обработку ошибок, отмену ненужных запросов (через cancellationToken например) и какой-нибудь denounce, чтобы не реквестать слишком часто. Решение сразу будет выглядеть элегантно и красиво.
markelov69
Объективно это не нужная и бесползеная процедура. Это иллюзия отмены, т.е. для того кто не понимает как работает HTTP запрос и бэкенд будет казаться, что он якобы что-то отменил. Но для как это на самом деле:
Браузер открывает TCP соединение, отсылает первые байты с данными в виде HTTP заголовков и тела запроса(если имеется), в это момент Nginx(или что угодно) проксирует этот запрос на бэкенд приложение(Node.js, PHP, C#, anyting else) и бэкенд начинает его обрабатывать, всё, обратного пути нет, но не может чудесным образом прерваться где-то в серединке. Он его обработает в любом случае целиком и полностью, но если вы его там прервется в браузере, то вы просто не получите ответа, любой супер тяжелый или нет запрос в БД улетит, отменяй/не отменяй это на клиенте. Так что это полная чушь и иллюзия для тех, кто не понимает как вообще работает бэкенд.
Ну как бы вот. Всё предельно просто и понятно, а главное читается сверху вниз, слева направо.
Надеюсь как реализовать функцию
debouncer
вы понимаете, но если нет, могу дополнительно поделиться реализацией, мне не жалко.Но если всё таки вы ярый фанат ненужных отмен, то это так же элементарно реализуется. И код будет выглядеть так:
Как видите ничего не изменилось, все так же элементарно и просто, читается сверху вниз, слева направо.
Vadem
Не обязательно. На бэкенде вполне можно(и во многих случаях нужно) обрабатывать прерванные запросы. В обработчике часто бывает больше одной тяжёлой операции, плюс многие операции на бэкенде тоже можно отменять.
Например, ASP.NET Core это может выглядеть примерно вот так:
Не везде и не всегда это имеет смысл, но забывать об этом не стоит.
markelov69
Самое страшное что вы правда в это верите и считаете применимым в реальности(( Будущее разработки в опасности(
А теперь будем честны сами с собой - 99.99% Вообще не испытывают никаких нагрузок и вообще без разницы, отправили вы лишний запрос или не отправили. Даже самая дешманская виртуалка будет легко обрабатывать ~1000 RPS и больше при условии что вы специально не пишете супер медленный код, супер не оптимальные запросы и т.п. Так вот 99.99% в пике имеют порядка 50 RPS, а в среднем это число не доходит до 10.
Теперь касаемо остальных: 0.01% в которых RPS хотя бы переваливает за 500, что тоже на самом деле является ничтожной цифрой с точки зрения вычислительных мощностей. А уж если они не развернуты на нормальной машине, так там не напрягаясь держаться тысячи и десятки тысяч RPS.
1) Никто никогда не пишет и не будет писать разумеется бэкенд, который после каждой строчки проверяет отмену на клиенте. Просто представьте себе этот шматок Г, который ещё убьет свои производительность на ровном месте.
2) Bottle neck это БД, а не бэкенд(c#, php и т.п.) что возводит в степень идиотизм в проверке на отмену.
3) В базах данных нет никакой отмены. Расслабьтесь. Скормили ей тяжелый запрос, она будет его выполнять пока не закончить, т.к. он синхронный! И выполняется в отдельном потоке/процессе в зависимости от БД.
4) Отменяй не отменяй, так же легко организовывается DDOS, которому пофиг на ваш говнокод с отменой на клиенте)
funca
Похоже на парадокс Абилина - мы делаем как придется потому, что все остальные, по нашему мнению, делают так же.
Не надо стесняться и оправдываться - пишите нормально, все будет нормально.
Vadem
После каждой строчки, очевидно, и не надо. Достаточно только перед началом каждой тяжёлой операции и/или её чанка. Часто это делается автоматически библиотеками/фреймворком.
Наоборот. Если ботлнек это БД, то нужно избегать лишних запросов в БД и отменять начатые как можно раньше.
Запросов в БД может быть больше одного. Запросы в БД можно отменять. Кроме БД есть ещё очереди, файловые и объектные хранилища, вызовы API других сервисов и т.д. Всё это могут быть достаточно дорогие операции и лишний раз их выполнять не нужно.
В базах данных есть отмена. Например, PQcancel в PostgreSQL. Более того, результат запроса из базы возвращается обычно потоком и считывается драйвером постепенно. Чем раньше вы отмените запрос, тем меньше нагрузка на базу.
Никто вроде и не говорил, что отмена запроса поможет как-то защититься от DDoS.
markelov69
Сначала лирическая часть, относится к 99.99% всех вэб проектов:
Даже если вы через какие-то дикие костыли худо бедно, в каком-то виде сможете убивать/останавливать запросы в БД(именно чтобы БД перестала тратить ресурсы на выполнение конкретного запроса), это вообще никакой роли не играет от слова совсем, т.к. мы же говорим про 99.99% проектов, про реальность, а не вымышленную популярность и загруженность, RPS пиковый 20-50 это просто комарик на слоне, при условии просто одной обычной виртуалки самой стандартной. +-1000 RPS в зависимости того насколько вы умеете проектировать схему БД, писать запросы и т.п. вообще на изи переваривается этой же виртуалкой, проверено многократными тестами само лично и нагрузочными в рамках компаний в которых работал. А если реально шарите и знаете что БД должна находится на Dedicated сервере и обладать 100% русурсами железа и систему, то уж извините, тут уже десятки тыщ это просто как семечки. Так вот, о опять возвращаемся в реальность да, 99.99% это в пиках ну 50 RPS, ну так и быть в аномальный промежуток времени пускай 300 RPS, это просто ни о чем, это просто около нулевая нагрузка на базу. БД способна обслуживать тысячами запросы, десятками тысяч, а на современном желе, а не на том что из 2010ых так уже наверное и к стоням тысяч походят циферки.
Теперь часть для 0.01% проектов, да даже меньше конечно, 0.01% это преувеличение:
Ремарка, когда речь идет о по настоящему высоконагруженных проектах (средний RPS в течении суток хотя бы > 5 000 и пиковые всплекски > 25 000), то безусловно для них очень атуально экономить ресурсы и процессорное время, в том числе отменять запросы, вместо всякого дерьма типа ORM писать запросы нормально и т.д и т.п.
Далее речь о чем-то промежуточном:
Давайте тут порассуждаем уже конкретно.
1) Вот вы говорите тяжелая операция.
- Тяжелая это сколько по процессорному времени? 1 секунда? 1 милисекунда? 10 микросекнуд? Давайте определимся с точной цифрой для оценки этого понятия.
2) Какой RPS в этом вашем проекте, какой RPS был максимальный на каких-то из ваших проектах?
Исходя из этих дынных можно что-то прикидывать, есть смысл, нету смысла заморачиваться.
Просьба на второй вопрос не писать наглую ложь и ответить честно. Я то легко отвечу чество, за всю мою карьеру, на продакшенах самый пикой RPS который я видел на мониторинге это что-то около 40, но это прям кратковременно, а средней в течении суток - 2 с хвостиком, это максималка которую я наблюдал ан разных проектах. Если бы RPS был выше в 20 раз, бэкенд бы вообще не напрягаясь переварил. Но тут ещё ремарка, это всё про то как я пишу код, архитектуру строю и т.п. Есть люди и "технологии" у которых на 20 RPS всё загнется, без шуток, лично такое наблюдал, было и очень смешно и очень грустно одновременно. В IT гигантах типо гугл и яндекс не работал, да и как бы не хочу. Там своих заморочек айда ушел)
nin-jin
Это несколько строк кода, если драйвер бд поддерживает аборты, чего вы раздуваете из мухи слона?
markelov69
Да без разницы, путь даже поддерживают(в чем я сомневаюсь, что прям все там чики пуки и вот прям прерывают выполнение синхронного кода БД(конечно нет)), что с того-то? Какая вам и 99.99% с этого польза? Для RPS не достигающих хотя бы нескольких тысяч, вообще нет никакой разницы если вы разгрузите бэк благодаря абортам на 1-3% в лучшем случае.
nin-jin
Я вам по секрету скажу, в нормальных фреймворках прикладнику об этом вообще думать не надо - оно само работает как надо.
nin-jin
Правильно написанный бэкенд может прерываться и на середине.
funca
Для такого случая и правда интересно - как хранится состояние, ведь utils пересоздается при каждом вызове fetchData.
markelov69
Неа, utils не пересоздается, а получает ссылку на один и тот же объект, вот подсказка:
P.S. Я бы рад отвечать быстрее, но из-за всяких недалеких, обиженных жизнью, которые заминусовали карму из-за того, что мое мнение не сходится с их мнением, приходится писать не чаще 1 раза в час. Понятное дело что серой массе комфортно находится именно в обществе серой массы, но что тогда делать тем, кто мыслит глубже и дальше?
funca
Интересно, тут не будет проблем с методами как в примере выше? В JS используется наследование прототипов и один и тот же экземпляр функции будет разделяться разными оюъектами, которые этот класс породил.
(по правде говоря я не проверял, но в теории это должно так работать).
Вызывая asyncUtils(this.fetchData) из разных объектов вы получите один и тот же экземпляр utils. Соответственно и состояние denounce внутри будет общим. Приятной отладки)
У вас манера высказываться довольно специфическая, хотя как специалист вы наверное хороший. Я обычно всем ставлю плюсики, кому отвечаю, даже когда не согласен по сути - раз появилось желание написать ответ, значит в этом что-то есть.
markelov69
Вы обратите внимание, я писал в примерах именно стрелочную функцию как метод класса, для не будет прототипного наследования, а значит и проблем о которых вы написали. Следовательно никакой отладки и никаких проблем)
Если хочется или принципиально нужно именно прототипное наследование для таких функций, то код будет выглядить так:
Тут this у каждого экземпляра свой собственный, отсюда и коллизий не будет. А универсальная реализация asyncUtils будет выглядить так:
Ну, скажем так прямая) Без виляний хвостиком) Понятное дело не всем нравится слышать правду, но а мне то что с того теперь) Элементарные вещи должны оставаться элементарными всегда. Искусственное усложнение чего либо это ярое зло.
mayorovp
Проблема-тио не во мнении, а в тоне.
nin-jin
amas1ov Автор
Ввел в заблуждение знаком препинания, прокси и остальную часть предложения нужно расцепить.
Вообще посыл ответа был таков, что нам приходится трястись над данными, например, обмазываться toJS
Я с МобХ работаю не так давно, и не могу давать какое-то "экспертное" мнение, вы в этом явно лучше.
Ваша проблема решается минутой гугления, не из головы взял
Вы правы, если цель - развернуть приложение, создать стор и крутануть счетчик. Речь шла про понимание самого подхода
Это рекомендации, как и название интерфейсов или представлений компонент и тд.
Напомню, что в начале статьи написал следующее (Не было цели сравнивать что-то, цель - ознакомительный характер, а ваши комментарии начинают нести придирчивый характер):
mayorovp
С пониманием подхода на MobX все очень просто: любой наблюдатель подписывается на те свойства, к которым обращается. И он подписывается строго сам, никакие там дочерние или родительские компоненты автоматически никуда не подписываются.
Соответственно, чтобы всё работало, нужно помнить следующее: любое обращение к реактивному свойству должно происходить строго внутри computed, autorun, reaction или observer. Если нужно прочитать значения из реактивной модели где-то еще — значит, надо или добавить autorun/observer, или же "доверить" чтение кому-то ещё. Видимо, потому вы и говорите об "обмазывании" toJS.
Но я бы не назвал это фразой "трястить над данными" — нужно просто понимать, вы читаете и передаёте изменяемые данные или неизменяемые. Если у вас есть изменяемая модель, а получателю нужна неизменяемая — да, надо сделать преобразование. Это, в конце концов, вопрос типизации (жаль только Typescript так не считает и позволяет их смешивать).
Alexandroppolus
Наблюдаемое может понадобиться в колбэке какого-нибудь пользовательского события/таймаута/етц. Просто значение "на данный момент", один раз. Потому опцию observableRequiresReaction считаю ненужной.
"Обратная" ей опция reactionRequiresObservable в общем полезна, но иногда может ругаться зря, когда observer-компонент при некоторых условиях таки не читает наблюдаемые.
nin-jin
Виртуализированный рендеринг сложных интерфейсов.
Редактор документов, электронных таблиц и тп вещей.
Растровая и 3д графика.
Даже просто создание кастомизируемой библиотеки виджетов - та ещё боль на Реакте.
funca
React Native? Мне казалось что он как раз чем-то таким и занимается.
Для canvas https://habr.com/ru/post/276585/
В браузере для плавных анимаций ещё нужна синхронизация с requestAnimationFrame. По сути это back pressure - когда продюсер изменений подстраивается под скорость их обработки получателем.
nin-jin
Это всё из оперы "троллейбус из буханки хлеба". Можете сами скачать, например, приложение Самоката на RN и полюбоваться тормозящими анимациями с задержкой в несколько секунд.
funca
Mobx это умный стор для тупых клиентов. Redux-like предлагают наоборот - примитивный стор для умных.
Пока логики немного, первый вариант выигрывает. Но развивая подход, вся сложность оказывается сконцентрированной в одном месте и оно становится узким. Дирежировать квартетом и дирежировать оркестром из 1000 инструментов это принципиально разные по сложности задачи.
У вторых сложность распределяется по клиентам и подход лучше масштабируется. Единственное, в разработке требуется дисциплина (лучше что-нибудь из области математики), иначе вся эта хореография быстро превратится в хаос. Effector предоставляет неплохой набор инструментов для такой композиции.
nin-jin
MobX - это не стор, а реактивная система, связывающая состояния. Стор на MobX - это MST.
funca
MST это state - компонент стора. Но спасибо за коммент, терминология это тоже важно.
markelov69
Мда, вот это "логика", тяжелый случай.
Ага, а ещё лучше из ядерной физики в перемешку с астрологией и битвой экстрасенсов.
Это где? В вашем выдуманном мире? Если вы сами ее сконцентрируете в одном месте, она там будет, если нет, то нет. Можете размазать, можете не размазать и т.п. Это вы пишете код, а не mobx, effector, redux и т.п. Как вы сами напишете, ровно так и будет. Отсюда все "беды", во всем в первую очередь виноваты кривые ручки, а во вторую уже выбранные технологии и подходы.
Увы и ах, нет.
mayorovp
Если вовремя вспомнить про декомпозицию — то нет, ни разу не становится. Надо лишь перестать притворяться, что SOLID — это что-то страшное энтерпрайзное, и что на фронте он не нужен. Нужен, как и куча других аббревиатур.
Нет ни единой причины сваливать всю логику ни в один класс, ни в один стор.
nin-jin
MobX: 16.3 kB + 2.2 kB = 18.5 kB
Effector: 10.4 kB + 3.7 kB = 14.1 kB
$mol_wire: 6.6 kB + ~1.5 kB = 8 kB
Ashot
С таким же успехом можно "запороть" проект redux'ом. Или mobx'ом. Или чем угодно. Так что я бы не стал так уж в лоб говорить о "запороть"(эффектором не пользовался, если что)