СоХабр закрыт.
С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.
DELETED
А если еще и оптимизацией заниматься в будущем, потому что иначе никак, то… ууу… целый квест…
Ну в общем-то автор к mobx и пришел в итоге, который ровно это и делает, только без бойлерплейта лишнего, плюс дополнительные возможности вроде абсолютной ненадобности всяких shouldComponentUpdate.
А кто Вам сказал, что я встречался с ОДНИМ таким приложением? Давайте не будем пользоваться ложными аналогиями.
чем самостоятельно написанное решение для работы с событиями может отличаться от решения модной библиотеки
Прежде всего, количеством человеко-часов потраченных на проектирование, разработку и тестирование решения. И, как следствие этого:
А если Ваш велосипед превосходит решение с гитхаба по всем этим показателям — самое время выложить его в OpenSource и написать про это на Хабр =)
лучшей архитектурой
Кто сказал?
более продуманным API
С чего Вы взяли?
лучшей документацией (она как минимум есть)
Допустим, но какая она и дает ли она ответа? У Zend Framework тоже есть документация, но люди все равно читают исходники.
меньшей когнитивной нагрузкой на новичков в проекте (не нужно читать исходники реализации)
Никогда документация не отражала сути вещей, а только базовые возможности. Не читать код нереально.
и наконец, большей надежностью
Кто Вам сказал? Почему Вы так уверены, что там нет какой-то заподлянки.
Все ваши утверждения надуманны и никто никаких гарантий не дает.
А мой велосипед будет выполнять необходимые задачи и от него больше ничего не требуется. И да, я публикую их время от времени.
Ну то есть Вы настолько уверены в себе и Вам кажется, что можете заменить команду fulltime-разработчиков, и десятки пользователей бета-тестеров? Мы же говорим не про проекты уровня left-pad. А про вещи, на которые будет завязана архитектура Вашего приложения. Т.е. взять и выпилить велосипед при обнаружении проблемы не получится.
И Ваш работодатель готов предоставить Вам время на проектирование, разработку, покрытие тестами и документирование велосипеда? По моему скромному опыту, если мы пишем какую-то инфраструктурную часть, то при покрытии тестами хоть пара багов да найдется.
Велосипед допустим и даже желателен, если он пристроен сбоку от основной функциональности. Или решает специфическую проблему. Но не когда уже есть 100500 стандартных реализаций.
Вы же сами пишете: «Вам не нужен Redux. Можете пользоваться Flux». Под Flux понимается архитектура или всё-таки библиотека? Если исключительно первое, то почему Mobx пользоваться можно? Если второе, то чем Redux хуже?
Flux — и архитектура, и библиотека: https://github.com/facebook/flux.
Ладно, это больше уже похоже на философский холивар. Тем более он уже был недавно на Хабре. В целом с Вашими мыслями я согласен.
Но вернемся к обсерверам. Писать на них каунтер понятно как. А если взять реляционные данные? Ну вот например модель этой статьи — Пост, Комментарии, Пользователи. В Redux я буду использовать нормализованные данные. В MobX — простые объекты, как в ORM на бекэнде. В mobx-state-tree я смогу использовать оба варианта. На уровне Stote удобнее нормализация, а в компонентах — объектное представление.
А вот как реализовать такое на MVVM? Какие понадобятся модели, вью-модели и обсерверы?
Если в MobX используется, как MV; компоненты, как V; и есть хранилище (M), то все будет аналогично. Большая часть людей использующих MobX и так реализуют MVVM.
MV
V
M
MobX
MVVM
Я не эксперт MobX и никогда не использовал его, видел статью, где делали MVVM с ним. Но в данном случае тогда все плохо, раз так, как Вы говорите. Бизнес-логике в компоненте-контейнере делать нечего. Контейнер-компонент можно использовать, как промежуточную часть между видом и бизнес-логикой, но никак не вместо бизнес-логики. Как это тестировать потом вообще? Имхо, не клева.
Если уж сравнивать подход MobX, с MVVM, то окажется что MVVM — это прошлый век =)
Есть Модель в виде реактивных объектов с данными и сервисов с бизнес-операциями. И есть Представление в виде дерева React-компонентов. В какой-то отдельной сущности ViewModel, которая оповещает компонент об изменениях модели, нет нужды. Потому что данные модели реактивны. Т.е. каждый объект данных сразу является и subject-ом. И они сами оповещают подписчиков о своем изменении.
Ну или можете считать, что вся логика VM реализована в общем виде в HOC observer() из mobx-react.
Кстати, точно так же работает и Vue, и Knockout, и $mol (этот даже вроде круче)
Слой сервисов — это и есть модель в терминах MVVM
Но в нем ничего не говорится про изменение состояния, которое распространяется по дереву с помощью контекста. В итоге при достаточно большом приложении мы придем к аналогу Redux с несколькими сторами.
Ну кстати да — вариант. Доменная часть состояния как правило присутствует и на сервере. Поэтому доменная логика отъезжает в мутации GraphQL. А для всякой глобальной презентационной фигни достаточно Component.setState() и React Context.
Во флакс-архитектуре (к которой относится и редакс) в сравнении с классической мобх-архитектурой:
(Дисклеймер: многое ниже верно только при использовании саг)
Экшены отвязаны от стейта (это важно, тк а) не приходится пробрасывать стейт для вызова метода и б) один экшен может запускать действия в разных частях приложения)
В редаксе разделено переключение состояния приложения и вызов бизнес-логике. В мобх в сервисы можно вынести только действия, не требующие переключения состояние в процессе. В итоге мобх методы превращаются в смесь из бизнес действий и переключения состояния
в принципе, редакс и мобх — это ООП против event-driven, с соответствующими плюсами: максимальной расширяемостью event-driven приложения (реагировать на события и производить свои можно, зная лишь контракт событий и метода диспатча, можно обрабатывать события других частей приложения. С ООП подходом так просто не получится). Взамен получаем очень низкую связность, иногда сложно сказать, что делает и к чему относиться конкретный экшен)
В итоге мобх методы превращаются в смесь из бизнес действий и переключения состояния
Ну, на бекэнде эту проблему давно решили разделением на DomainLayer (переключение состояния модели) и BusinessLayer (эффекты). Кто мешает сделать такьв MobX?
Я имел ввиду ситуацию типа следующей:
Все это — и бизнес действия, и переключение состояния приложения — идёт вперемешку.
Пример простой, но в реальных приложениях сценарии могут быть сложнее, и код получается большой и запутанный.
Редакс отделяет эти переключения флагов и тп в редьюсер, а бизнес логика остаётся в саге.
А чем принципиально отличается
yield put({ type: "MY_DATA_LOADING" })
От
MyDataService.setLoading();
?
Разве что в первом случае у нас слабое связывание за счет pub/sub (action/reducers). Ну так во втором случае мы можем получить объект MyDataService с помощью Dependency Injection.
Принципиально — тем, что экшен может обрабатываться в нескольких местах без передачи дополнительной зависимости.
Такая ситуация возникает только если программисту не нужно это самое разделение. В противном случае управление флагом легко выносится в отдельный хелпер и всё.
А еще есть вот такая штука в библиотеке mobx-utils:
class Foo { @observable todo = null; @action doRequest() { this.todo = fromPromise(fetch("https://jsonplaceholder.typicode.com/todos/1")); } get isPending() { return this.todo.state == "pending"; } // ... }
Ну и с чем именно тут перемешана бизнес-логика, по-вашему?
Конкретно в этом примере — конечно же, ничего не перемешано. В более сложных случаях может быть невозможно вычислить состояние представления, и приходиться мутировать, тогда будет перемешан код мутации и бизнес логика.
Я использовал саги в десятке приложений, и не могу на них нарадоваться. «Магия саг», конечно, присутствует, но не особо мне мешает.
Мне кажется, что redux-saga вообще стоит рассматривать не как библиотеку, а как DSL для сайд-эффектов. И как всякий DSL он отлично выполняет задачи в своей области применения. Лучше, чем например RxJS, который скорее библиотека для того же самого.
Но, как любой новый язык, он требует более глубокого изучения, чем просто библиотека. Отсюда и ощущение "книги заклинаний". Например, я был в шоке от такого (пример из оф. доки):
try { while (true) { let seconds = yield take(chan) console.log(`countdown: ${seconds}`) } } finally { if (yield cancelled()) { chan.close() console.log('countdown cancelled') } }
Как вообще код выйдет из while (true) без break и return? Но потом я почитал внимательно доки по генераторам и понял, что все норм. А представьте, что подумает джун увидев такое?
И разумеется, DSL имеет свои границы применимости. Поэтому не надо пихать саги туда, где не ожидаются сложные эффекты. Загрузить данные по клику на кнопку с показом "Loading..." — это не сложный эффект.
Ну, собственно, библиотека, предоставляющая подобие DSL для сайд-эффектов, она так себя и позиционирует.
Этот код ставит в ступор не только джуна. Код хорошо описывает заморочку: саги в самом деле концептуально как ремонт — они могут не заканчиваться ни когда, их можно лишь прекратить. Если на клиенте с абстракциями в стиле CSP (Communicating Sequential Processes) вполне можно жить, хотя вся эта затея и выглядит как прикол (в конечном счёте страницу перезагрузят), то при рендере на сервере все же удобнее работать с конечными штуками.
Ну обычно саги с сохранением состояния, возобновляемые по внешним событиям, на сервере и не нужны (и не должны запускаться)
При изменении view model хотим рассылать оповещение, потому либо а) генераторы свойств модели, прокси либо б) единый способ установки свойств ala dispatch
Если свойство запускает оповещение, то установка каждого из них может менять подписчиков синхронно. Было бы круто, чтобы вместо этого все могли дождаться окончания изменений и перерисоваться один раз. Кроме того, обработчик изменения firstname может желать делать разные вещи, в зависимости от того, изменился ли lastname вместе с ним. Решения — а) хитрые биндинги и подписки или б) отказ от обновления индивидуальных свойств, ala payload в dispatch.
На моей практике view model проще, до определенного момента, для простых ситуаций всем проще выбрать а). По мере роста сложности и взаимосвязей в UI все больше хочется выбрать б) Количество boilerplate в redux ужасает, явно можно сделать лучше.
Вся проблема в том, что никто не объясняет зачем нужен и когда нужен Redux
вызывать экшен, который вызовет обновление хранилища, что вызовет перерисовку всех подписанных контейнеров, а за ними и компонентов
Нет. Обновятся только те компоненты, которые зависят от изменённых данных.
И в чём разница по сравнению с вашим подходом на обсерверах?
Либо все компоненты пользуются одним общим списком, тогда ре-рендер закрытых контролируем условием isOpen в shouldComponentUpdate, либо на каждый компонент заводится отдельное поле, тогда ре-рендер будет контролироваться автоматически в PureComponent. В случае с обсерверами у вас то же самое условие isOpen будет лежать в componentDidUpdate для подписки/отписки от модели.
isOpen
function findUsers(query) { return dispatch => { dispatch({ type: SEARCHING_USERS, payload: true }); searchUsers(query) .then(foundUsers => { dispatch({ type: SET_FOUND_USERS, payload: foundUsers }); }) .finally(() => { dispatch({ type: SEARCHING_USERS, payload: false }); }) }; } function reducer(state = initialState, action) { switch (action.type) { case SEARCHING_USERS: return { ...state, searchingUsers: action.payload }; case SET_FOUND_USERS: return { ...state, foundUsers: action.payload }; default: return state; } } class Dropdown extends Component { state = { isOpen: false, query: '' }; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(nextState, this.state) || (this.state.isOpen && (nextProps.foundUsers !== this.props.foundUsers)); } handleSearchChange = (query) => { this.setState({ query }); this.props.onChange(query); }; }
Да, но их три, а не один, а следовательно все, кроме Dropdown умножаем на три.
Dropdown
Нет. Во-первых, вы не можете использовать все три поиска одновременно. Если же всё-таки нужно использовать независимые хранилища, то просто передаём ключ:
function findUsers(query, key) { return dispatch => { searchUsers(query) .then(foundUsers => { dispatch({ type: SET_FOUND_USERS, payload: { foundUsers, key } }); }) }; } function reducer(state = initialState, action) { switch (action.type) { case SET_FOUND_USERS: return { ...state, foundUsers[action.payload.key]: action.payload.foundUsers }; }
Да. И нужно будет всегда следить за этими ключами, таскать их, как константы. Мне, лично, не особо такой подход. Приятно, когда есть просто метод и ты не задумываешься о том, как там внутри данные следует организовать, чтобы все работало. Просто сделаю все необходимое в ViewModel и сокрою все эти нюансы.
ViewModel
А зачем вам их везде таскать в данном случае?
Если вам нужно сохранять ещё и предыдущее состояние, то это сделает редьюсер. Для запроса логично каррировать findUsers.
Дело в том, что вы пишете:
появилось центральное хранилище, которое будет расти по мере роста самого проекта и заставлять ререндириться всем компонентам каждый раз после обновления хранилища. Чтобы этого избежать придется повозиться.
В большинстве случаев достаточно PureComponent.
В целом, такой подход имел бы право на жизнь, если приложение не содержит бизнес-логики, которую, по сути, логично размещать в экшнах (а именно обращаться к нужным сервисам из экшнов). Но данное решение приводит к тому, что придется каждый раз, когда нам потребуется перерасчет (например, скидки за товар на основе цены, количества и действующих акций) вызывать экшен, который вызовет обновление хранилища, что вызовет перерисовку всех подписанных контейнеров, а за ними и компонентов.
В любом подходе если у вас изменились данные в модели вам придётся обновить те компоненты, которые отражают эти изменившиеся данные.
А ведь еще нужно и передать все эти данные в экшн! И так по кругу. Представьте форму из 20+ полей и все эти поля нужно постоянно передавать по кругу.
Если используется, например, redux-thunk (а как минимум он будет присутствовать, если есть запросы к серверу), то он передаёт getState в экшен. По кругу пробрасывать не потребуется.
И чем больше приложение, тем глубже и больше дерево редьюсеров
Дерево для того и дерево, чтобы разносить логику.
Я не говорю, что Redux — идеальный подход. У него есть свои недостатки, но они совсем другие.
Тогда нарушается принципы, которые заложены из Flux: action ->… -> store, и только в этом направлении. Никак иначе. Дергать store-данные из экшена это уже костыль.
Flux
action
store
Я бы с удовольствием почитал Ваши мысли по этому поводу.
А асинхронный подход redux-thunk, строго говоря, уже не ложится на Flux-архитектуру, потому что асинхронные экшены там порождаются не из View-слоя. Если же считать, что они всё-таки вызываются View-слоем, только асинхронно, то противоречия нет, потому что View имеет доступ к чтению Store.
Основной принцип Flux всё-таки в том, что View не имеет права непосредственно изменять Store. Эта архитектура отражает более общий принцип CQR (Command-Query Segregation), разделяющий получение данных (Query) и их изменение (Command). В данном случае action creators так же не изменяют данные (это забота редьюсеров), а только запрашивают их. Принцип CQR не нарушается.
Thunk может сам играть роль view? В архитектуре же не сказано, что вью одно и оно обязательно реакт. А так, сам по себе реакт это просто функция.
Теоретически да. И react берёт данные из стора и «асинхронно» диспатчит экшены, и thunk может брать данные из стора и асинхронно диспатчить экшены. Разница только в том, что react получает данные пассивно через connect, а thunk — активно, вызывая getState.
Дерево для того и дерево, чтобы разносить диспетчеры .
За счет того, что CartPage имеет приоритет выше, чем наши компоненты, мы смогли добиться взаимодействия между ними (компонентами).
Между тупыми компонентами. Самое веселье начинается, когда надо связать два умных компонента, не вынося все их мозги на ещё один уровень вверх.
запросы к API — это, совсем другая ответственность, а следовательно, нам нужно их где-то разместить. Для этого существуют репозитории. Вот и все. Прозрачно и понятно.
И неправильно.
В случае ошибки при загрузки, приложение повиснет в состоянии ожидания.
При любом ремаунте (а в Реакте они могут происходить по куче безобидных причин) будет происходить повторный запрос данных.
Запросы происходят для каждого места, где эти данные нужны, никакого шаринга.
Осталось написать HOC, где будет автоматически происходить подписка на изменения в View Model:
Самое веселье начинается, когда оказывается, что для вью-модели требуется несколько моделей. И в качестве изюминки — случаи, когда от значения одной модели зависит то, какая ещё модель требуется.
Без бойлерплейта, с API — https://codesandbox.io/s/pk19r1ov7m
Про отсутствие бойлерплейта — смелое заявление. У вас получилось 155 строк. В то время как то же самое можно было бы описать в 35 строках на более других технологиях:
Компонент с кнопкой:
$my_cart_details $mol_view quantity?val 0 sub / <= Increase $mol_button click?event <=> increase?event null title \Increment
Добавленная логика инкремента:
class $my_cart_details extends $.$my_cart_details { increase() { this.quantity( this.quantity() + 1 ) } }
Компонент со статистикой:
$my_cart_info $mol_view sub / <= prefix \Quantity: <= quantity 0
Вьюшка приложения (она же "точка входа"):
$my_cart_app $mol_view quantity?val 0 sub / <= Info $my_cart_info quantity <= quantity <= Cart $my_cart_details quantity?val <=> quantity?val
Добавленное асинхронное взаимодействие с сервером:
class $my_cart_app extends $.$my_cart_app { quantity( next? : number ) { return this.$.$mol_http.resource( '/api' ).json( next ) } }
Зачем оно там повиснет? Ошибки нужно отлавливать.
Нужно, но вы это не реализовали. Поленились/забыли — не важно. Важно, что в реальных проектах подобных косяков вагон.
Если Вы сделаете так, чтобы они запрашивались каждый раз — так и будет.
Как правило, мы хотим получать свежие данные при открытии страницы. Очень часто маунт используют для детектирования открытия страницы и инициализируют запрос/обновление данных. А так что любой ремаунт приводит к запросу всех данных.
Эти проблемы каждый решает сам, но мне лично непонятно зачем хранить устаревшие данные, если они и так могут в любой момент измениться. Какие-то можно закешировать, но не все.
Тут речь шла про то, что одни и те же данных могут требоваться для отображения нескольких разных виджетов на одной странице. Чтобы не запрашивать их каждый раз — они должны шариться. Для этого обычно служит слой модели.
'Самое веселье начинается, когда оказывается, что для вью-модели требуется несколько моделей.' Значит, что-то пошло не так.
Что значит не так? Это типичная задача. Например, статья, комментарии и пользователи, как выше отметили.
Это как раз я рассматриваю в конце статьи.
Вы про костыль с ручной подпиской и как обычно забытой отпиской? :-)
Нужно, но вы это не реализовали.
Есть множество замечаний к коду. И, прошу заметить, я нигде этого не сделал. Как мне кажется, это очевидно. Или нет? =)
Это типичная задача.
Как часто Вам требуется общий стор? Мне не особо. Когда это требуется — проблем его добавить нет.
Почему костыль? Что-то начало рендериться, в ViewModel мы подписались на изменения. Если что-то измениться — все обновиться. И отпиской от чего и почему забытой?
Вообще, по моему опыту, бойлерплейта в редаксе много только в масштабе маленького приложения.
В более-менее объёмном приложении весь бойлерплейт покрывается фабриками стандартных редьюсеров, саг, селекторов и тп
Получается весьма лаконично и понятно, благодаря функциональному подходу.
Для уменьшения бойлерплейта с экшенами использую такой подход: https://github.com/rd-dev-ukraine/rd-redux-utils/blob/master/readme.md
Реализация — в десять строк.
Тогда вам в ангуляр :)
Мне кажется, дело во многом в функциональной природе редакса, где многие вещи проще написать самому, чем разбираться в чужих
Что вы думаете на счёт хуков в последних версиях react? По-моему они предоставили вполне аккуратный интерфейс, чтобы не так сильно запинаться о сайдэффекты.
Все эти хуки — костыли для прикручивания lifecycle к функциональным компонентам.
Реакт отродясь был функциональным. Собственно отсюда все заморочки, когда вопросы с сайдэффектами пытаются по привычке решать императивно. Хуки это не только lifecycle.
Но хуки только внешне выглядят функциональными, а на самом деле внутри модифицируют компоненты как в ООП.
Вам не нужен Redux
Может лучше сразу было на битву экстрасенсов идти? Смелые заявления без наличия входных данных по задаче.
Вся проблема в том, что никто не объясняет зачем нужен и когда нужен Redux, пока ты не наступил на эти грабли спустя время.
Это вопрос документации и квалификации разработчика, а не нужности библиотеки.
И в тех самых местах, где он оправдан, я бы также его убрал и использовал события.
Как подтверждение второй части.
Но люди, почему-то, ссут подумать своей головой.
Написав такую статью с таким заголовком вы меня убедили, что это не самое плохое решение со стороны людей.
И сейчас я вам поведаю сказ о том, как жить без Redux.
До его появления все как-то жили, может лучше было провести урок истории?
Ну а если по содержанию, с нормальным заголовком, хорошей подачей информации и исследованием существующих библиотек, это могла быть адекватная(но потенциально очередная) статья с обзором разных подходов к организации данных на фронте, но не в этом случае. Новичку лучше будет почитать комментарии со ссылками на либы.
Вообще перед написанием статьи стоит сначала определить для кого она и с какой целью пишется. «Убедить весь мир, что редукс — г-но» — очень плохое решение для этих двух пунктов.
Холиварить не планирую, серебрянной пули всё равно нет.
комментарии (94)