Comments 166
— Забыть уже наконец об этом ужасе, который тянется с давних пор, открыть глаза и использовать MobX.
1) Как на 1 событие изменить 2 независимые части стора?
2) Как создавать динамические хранилища? имею ввиду, если список полей одного из куска стора, известен только во время выполнения?
ну и такая мелочь, что бизнес логика и обновление стора идет в одном месте, может это во мне привычка говорит, но идея эта мне не очень нравится
Вот вам живой пример — codesandbox.io/s/ecstatic-cloud-l956n
Вообще на дворе 2020 год и мне кажется дико когда React developer'ы не знают что такое MobX и даже не пробовали его, ладно ещё в 2016 году, но в 2020 это нонсанс конечно)
P.S. я заменил Redux на MobX ещё в 2016 году и с тех пор получаю удовольствие от работы с реактом, до этого было больше боли из-за редакса.
пример:
изменилось поле А, я его проверил, оно корректно, мне надо сделать запрос на бэк, получить данные других полей и обновить их так же. (изменили ZIP code, надо получить город и штат)
это все будет в том же месте, где я буду обновлять основное поле в перемешку?
— Есть проекты где начинают писать версию «2.0».
— Есть проекты которые можно рефакторить и заменять redux на mobx.
— Во многих вакансиях указан только Redux, но по факту никто не против внедрить mobx.
Критической проблемы в этом нет, да большинство существующих проектов к сожалению написаны на redux'e, но тут причина банальная, люди не поднимают голову и не смотрят по сторонам. И/или не рискуют попробовать что-то другое.
Я например с 2016 года проекты только с mobx'ом пишу и вообще кайфую. До этого меня раздражал реакт из-за редакса, а с момента как узнал про mobx то эта связка стала идеальной для меня.
Ну у нас до недавнего времени MobX не упоминался в вакансиях, собственно даже знание React "будет плюсом", поскольку практика показла, что нормальный фронтендер довольно быстро осваивает и React, и MobX
* Redux раньше появился.
* Redux хорошо пропиарили в начале и до сих продолжают.
* Как следствие пиара, большинство работали только с redux.
* Как следствие пиара, большинство библиотек компонентов, темплейтов используют redux.
* В redux также описана архитектура, поэтому понятно как с ним сделать одинаковую структуру для разных страниц проекта. В случае mobx нужен соответствующий опыт, либо самому до этого додуматься.
* Абрамов прям супер ит-евангелист. Несмотря на его небольшой опыт, к нему прислушиваются большинство. Какую бы чушь он не продвинул в мире реакта, большинство реакт-разработчиков будут ею пользоваться.
* Команды бэкендеров, фулл-стэков переходя на написание SPA, смотрят, что сейчас популярно. Ага, react, redux. Будем тоже это использовать. Новички в веб-разработке поступают аналогично.
Так на 1 событие изменить 2 независимые части стора?
Очень просто: взять и изменить.
@action foo() {
store.bar.x++;
store.baz.y = "Hello, world!";
}
Разместить этот метод можно в любом классе, который вы считаете подходящим для этой цели.
Как создавать динамические хранилища? имею ввиду, если список полей одного из куска стора, известен только во время выполнения?
Используя observable map, observable object или же можно класс динамически создать (у нас же javascript, в конце концов).
ну и такая мелочь, что бизнес логика и обновление стора идет в одном месте, может это во мне привычка говорит, но идея эта мне не очень нравится
Пишите в разных, никто не запрещает же
Разместить этот метод можно в любом классе, который вы считаете подходящим для этой цели.
получается будут огромные экшены, меняющие разные, не связанные части стора, это не страшно, если кода не много, но на проекте котором я работаю, будут сложности. Хотя, как я понял, можно просто добавлять доп. экшены, которые будут срабатывать на изменения полей.
Пишите в разных, никто не запрещает же
опять же, придется напрямую вызывать эту логику из экшенов, и вслучае добавление новой, надо опять, залазить в экшен и добавлять вызов, в случае с сагой, обновление стора зависит только от экшенов
получается будут огромные экшены, меняющие разные, не связанные части стора
Ну а сейчас у вас, видимо, огромные саги, вызывающие разные, не связанные экшены...
Если же огромных саг у вас не наблюдается — почему экшены в MobX должны магическим образом стать таковыми?
опять же, придется напрямую вызывать эту логику из экшенов, и вслучае добавление новой, надо опять, залазить в экшен и добавлять вызов
Не понимаю о чём вы пишете.
Если же огромных саг у вас не наблюдается — почему экшены в MobX должны магическим образом стать таковыми?
саги не большие, это достигается тем, что на 1 экшен реагирует несколько различных саг, отвечающих за свой кусок логики. И как я написал выше, в случае с mobx, как я понял, будут экшены реагирующие на изменение части стора и изменять свою соответственно
Не понимаю о чём вы пишете.
если я правильно понял
Пишите в разных, никто не запрещает же
Вы имели ввиду, вынести логику в сервисы, и вызывать их из экшенов
@action foo() {
store.bar.x++;
store.baz.y = someService.someProcessing(store.bar); //какая-то бизнес логика
}
и это, мне не очень нравится, я об этом говорил выше
Если вам так нравится слабая связность — не одна только саги её вам могут обеспечить. Простейший EventEmiter пишется за пару минут...
и это, мне не очень нравится, я об этом говорил выше
Альтернатива-то какая?
Если вам так нравится слабая связность — не одна только саги её вам могут обеспечить. Простейший EventEmiter пишется за пару минут...
немного сомнительно, я использую только часть возможностей саг, однако, даже эту небольшую часть реализовать руками, займет время, мне проще добавить сахара к имеющемуся, что и вправду быстро.
Альтернатива-то какая?
я без понятия, в сторону mobx только начал смотреть, может Вы знаете решения
Как на 1 событие изменить 2 независимые части стора?Непонятно, что за события.
Ну, допустим, нам по веб-сокетам инфа какая-то пришла? У вас есть листенер сокетов. В этом листенере должны быть ссылки на 2 нужные части стора. Когда приходит событие — обновляете одну часть стора, а потом вторую часть стора.
Как создавать динамические хранилища? имею ввиду, если список полей одного из куска стора, известен только во время выполнения?Это вообще непонятно. Как работать неизвестно с чем? Вы к каким полям обращаться потом будете? Можно практический пример? Я за свою практику никогда не видел такой странной задачи.
бизнес логика и обновление стора идет в одном месте, может это во мне привычка говоритА где, по вашему, обновление стора должно лежать?
Непонятно, что за события.
я имел ввиду, ввод данных пользователем, и я понимаю, что из одно экшена мы можем изменять весь стор, но это приведет в сильно большим экшенам(action()). Но это вначале были мои мысли, сейчас я разобрался как этого избежать.
Это вообще непонятно. Как работать неизвестно с чем? Вы к каким полям обращаться потом будете? Можно практический пример? Я за свою практику никогда не видел такой странной задачи.
куски кода менее понятны, я просто опишу задачу. Нужен инструмент для создания форм, есть набор полей, которые будут почти в каждой форме, однако, он может отличаться, в зависимости от формы, причем формы могут содержать уникальные поля, присущие только ей.
Для решения этого, я собираю имена полей и регистрирую их сторе при старте и работаю с ними по единому интерфейсу для полей, но на этапе разработке их имен я не знаю.
А где, по вашему, обновление стора должно лежать?
обновление стора правильно лежит, меня смутило, что там не только обновление стора, но и вся логика лежит, в случае с redux-saga, редьюсеры отвечают только за обновление стора, а бизнес логика лежит в сагах
Нужен инструмент для удобного создания форм, есть набор полейЯ б делал это через массив типизированных инстансов. С массивом в JS работать значительно удобнее, чем с хешем.
Я б делал это через массив типизированных инстансов. С массивом в JS работать значительно удобнее, чем с хешем.
если это сделать через массив, очень много придется перебирать эти массивы для поиска, изменения и удаления. Как это выглядит в коде, мне абсолютно не понравилось, а с хэшами, немного приятнее, но тоже не обошлось без неудобств.
очень много придется перебирать эти массивы для поиска, изменения и удаленияУчитывая, что в JS при добавлении в хеш и удалении из хеша пересоздаётся внутренний класс — выгоднее делать это классически через фильтрацию, к примеру. А сортировать как-то иначе хеш вообще нереально)
Учитывая, что в JS при добавлении в хеш и удалении из хеша пересоздаётся внутренний класс — выгоднее делать это классически через фильтрацию, к примеру. А сортировать как-то иначе хеш вообще нереально)
Вопрос не в производительности, а в читаемости. для обновления значения в сторе используя массив, код будет выглядеть примерно так:
[fieldsActions.changeFieldValue] = (state, payload) => {
const index = state.indexOf(payload.fieldName);
const nextState = [...state];
nextState.splice(index, 1, {
...(state[payload.fieldName] || initialFieldData),
enteredValue: payload.value,
});
return nextState;
};
используя хэш так:
[fieldsActions.changeFieldValue] = (state, payload) => ({
...state,
[payload.fieldName]: {
...(state[payload.fieldName] || initialFieldData),
enteredValue: payload.value,
},
});
на просто примере разница кажется не сильно большой, но в реальном проекте, читаемость сильно отличается
@action deleteItemByIndex (index: number) {
this.items = this.items.filter(it => it.index !== index);
}
А изменение вообще просто делается:
field.value = newValue
Просто ваш хреновый инструмент заставляет вас крутиться как ужа
В Mobx это пишется куда проще:
@action
function changeFieldValue(payload) {
payload.field.enteredValue = payload.value;
}
Причём это работает одинаково что для массива, что для хеша...
payload.field.enteredValue = payload.value;
А что странного-то? Ну, за исключением сомнительной необходимости функции changeFieldValue, но её не я придумал.
А что делать, раз пайлоад такой получился? Всего-то fieldName
(идентификатор модели) на field
(саму модель) заменил, как это в mobx принято.
Я мог бы вовсе убрать функцию changeFieldValue
, но тогда кто-то мог бы спросить: "а куда функция делась?" :-)
В мобиксе, конечно, нету как в Редаксе никаких странных пейлоадов, куда надо впихнуть всё, ибо больше некуда.
class FieldModel {
@observable title;
@observable value;
@action setValue = (newValue) => {
this.value = newValue;
}
}
<FieldView
title={fieldModel.title}
value={fieldModel.value}
onChange={fieldModel.setValue} />
В мобиксе, конечно, нету как в Редаксе никаких странных пейлоадов
В защиту redux (да знаю, что меня за одно только это слово побьют) скажу, что нет реально никаких причин использовать payload
. Это какое-то коллективное помешательство. Единственное зарезервированное поле в action-ах это type
. Моя не понимать, зачем все так страдают.
От замены payload на action странность не пропадает...
Разок пришлось писать на redux вместе с другой командой. Местами там писали примерно такое:
// компонент
dispatch(updateUser(id, fisrtName, lastName, email, phone));
//action
updateUser = (id, fisrtName, lastName, email, phone) => ({
type: UPDATE_USER,
id,
firstName,
lastName,
email,
phone,
});
//saga
function* updateUser({ id, fisrtName, lastName, email, phone }) {
yield userApi.update(id, fisrtName, lastName, email, phone);
//api
const update = async (fisrtName, lastName, email, phone) => api.patch(`/user/${id}/`, {
fisrtName, lastName, email, phone
});
Как по мне, чем меньше участков кода знает о структуре передаваемых данных, тем лучше. Меньше кода и меньше вероятность ошибок. Вариант с payload:
// компонент
dispatch(updateUser({ id, fisrtName, lastName, email, phone }));
// action
updateUser = (payload) => ({
type: UPDATE_USER,
payload
});
//saga
function* updateUser({ payload }) {
yield userApi.update(payload);
//api
const update = async ({ id, ...payload }) => api.patch(`/user/${id}/`, payload);
Да я абсолютно убеждён, что большая часть classic-redux way это мусор. И слово payload в том числе.
Как по мне, чем меньше участков кода знает о структуре передаваемых данных, тем лучше
У нас TypeScript. Каждый участок знает ровно то, что ему положено знать и ни на грамм больше. И всё статически завязано и гарантировано. Спасибо структурной типизации и генерикам.
Меньше кода и меньше вероятность ошибок
Вы кстати хороший такой антипаттерн нарисовали. У вас всё пробрасывается вслепую вплоть до api. Это очень плохо. Особенно без TypeScript-а. По сути у вас бомба замедленного действия. Вы напрямую связали удалённые участки кода из разных слоёв. Малейшее изменение с одной стороны приводит к непредсказуемым (и возможно трудноотлавливаемым) багам в конце цепочки.
Кстати, вы не сказали, что вместо payload использовали бы в redux, если бы пришлось на нем писать? Или вы имели ввиду просто давать этому объекту более подходящее именование?
Кстати, вы не сказали, что вместо payload использовали бы в redux, если бы пришлось на нем писать? Или вы имели ввиду просто давать этому объекту более подходящее именование?
Просто все необходимые поля располагаю прямо внутри action-а. На 1-м его уровне. Там же где и поле type. К чёрту бюрократию.
{ type: string, value: Partial<Unit> }
Эти комбинированные типы — как-то пошло. Всё-таки тип на более высоком уровне иерархии.
Просто не должно быть дурацких абстрактных названий типа «payload», дурацкой динамической типизации с совершенно непонятными и неконтролируемыми структурами и, конечно, дурацкого редакса
Дык в TS структурная типизация, а не номинативная. Не испытываю решительно никаких проблем с этим. Все типы как нужно выводятся и никакой type
где попало не всплывает. Правда я забыл уточнить что у нас 100500 своих велосипедов и суммарно это всё не похоже на redux (хотя он всё ещё под капотом). Это несколько меняет дело. Если делать всё по заветам Ильич… Абрамова, то там везде грусть конечно. Но грустить некогда, надо писать копипасту во имя ынтерпрайза
{ type: string, value: Partial<Unit> }
Ну я и не предлагал поля Unit пихать на 1-й уровень action-а. Но помимо value: Partial<Unit>
там ещё могут быть какие-нибудь дополнительные поля, не из Unit. И всё это будет на верхнем уровне вместе с value
(maybe diff
?). Т.е. я не предлагаю уплощать всё до неадеквата. Я просто предлагаю избавиться от лишнего уровня иерархии в виде некоего payload
-а
Как по мне, чем меньше участков кода знает о структуре передаваемых данных, тем лучше.
Вечная борьба между связанностью и связностью.
Массивы перебирать вам не придётся если вы сохраните ссылку на элемент вместо имени поля.
Массивы перебирать вам не придётся если вы сохраните ссылку на элемент вместо имени поля.
не совсем понял Вашу мысль, я инициализировал список полей как массив, потом пользователь вводит данные в одно из полей и мне надо в торе изменить значение value. Как это сделать, не перебирая массив в поисках нужного поля?
store.form.fields.map(field => {
// ...
return <input ... onchange={e => field.setRawValue(e.target.value)} />
});
Для производительности тут надо обязательно отдельный компонент выделить (или хотя бы Observer), но это уже детали.
У вас же есть метод setRawValue
. В него любую валидацию можно добавить...
обновление стора правильно лежит, меня смутило, что там не только обновление стора, но и вся логика лежит, в случае с redux-saga, редьюссеры отвечают только за обновление стора, а бизнес логика лежит в сагахЯ придерживаюсь того, что в любом сторе могут храниться только данные этого стора и методы для работы с этими данными (кэширование, преобразование данных перед их сохранением в стор или передачей в компонент).
Фактически этим и занимается mobx, vuex и даже redux. Только в redux это усложнено и размыто по куче ненужных сущностей.
Еще недавно стал считать нормальным подход (но не пробовал его), где api метод (или посредник с сайд-эффектами, вроде redux-saga) вызывается в методах стора и после выполнения стор сохраняет пришедшие данные. Но только вызов, преобразование и сохранение данных. Никакой сетевой логики, сайд эффектов в коде стора.
Наличие другой логики, кроме описанной (но я может что-то и упустил), превращает стор в какой-то контроллер.
Может в примерах mobx и не сделано, как я описал. Но там можно так организовать архитектуру. Конечно, минус mobx-у за то, что нет рекомендаций (я не видел), best practices по архитектуре.
Только в redux это усложнено и размыто по куче ненужных сущностей.
Я как раз люблю слабую связанность кода, и разбитие на слои, мне больше нравится, чем иметь только контроллер и сервисы
В более полном виде выглядело бы так:
* слой взаимодействия с сервером
* слой сайд-эффектов
* слой стора (хранения глобальных данных и уведомление об изменениях в этих данных)
* слой логики компонента (custom hooks)
// я тоже предпочитаю разбивать на слои и не считаю нормальным, что одну и же логику можно писать в двух разных сущностях (в компоненте и в custom hook)
* слой view (jsx)
см. final-form
Если у нас архитектурно все состояния хранятся в глобальном сторе, то формы обычно не выделяют во что-то особое. У нас вот сейчас мощное "лобби" в пользу отказа от final-form, потому что есть другой менеджер состояния.
MobX, что-то вроде ViewModel на его основе для страниц и форм (у нас грань между ними очень тонкая). У модели этой для форм computed свойства типа isVaild и errors.
Основные аргументы против final-form, насколько я помню — большой порог входа в саму либу и несколько способов (react state, mobx, final-form и что-то ещё типа react-i18n) работать со стейтом в проекте в целом, что ещё больше повышает порог входа в проект в целом. Типа "пускай безобразно, но единообразно"
Зачем о них забывать?
2) Быть может это просто вы пока слишком далеки от этого из-за нехватки квалификации, не понимая асинхронности, не понимая реактивности?
Но хуки из коробки не предоставляют никакого механизма подписки на изменения частичного стейта из useReducer
для компонентов-потомков в глубине дерева.
MobX один из худших Стейт менеджеров. Низкая производительность, плохая масштабируемость, сложный код, лишние конструкции, которые провоцируют писать лишнее.
о, а можно пруфы про низкую производительность, сравнения какие-нибудь?
Но и MobX всего лишь инструмент. Способов выстрелить себе в ногу там довольно много. И с отладкой в относительно большом приложении там беда-беда. Так что об ужасе redux я бы так категорично не заявлял.
Но и MobX всего лишь инструмент. Способов выстрелить себе в ногу там довольно много. И с отладкой в относительно большом приложении там беда-беда.
Правда?
1) И как же можно выстрелить себе в ногу?
2) С отладкой беда-беда? Подробнее и желательно конкретный пример.
P.S. новичков и не опытных мы в расчет не берем, потому что этот инструмент не для них, в силу их неопытности и не понимания многих вещей.
Помнится, недавно на проекте была какая-то проблема с наследованием классов. Не наследовались полноценно.
Тогда ни у одной библиотеки проблем нет
была какая-то проблема с наследованием классов
Ну какие могут быть проблемы, когда конкретного кода нет, в котором проблема проявляется. С таким же успехом любой может сказать, Vue это фигня, потому что я сделал todo list и у меня все тормозило. И что, после этого принимать эти слова на веру потому, что кто-то так сказал?)
class WithCounter {
@observable count = 0;
incr = () => {
this.count++;
}
decr = () => {
this.count--;
}
}
class WithWeakSetRefs {
set = new WeakSet();
addRef = (ref) => {
if (ref !== null) {
this.set.add(ref);
}
}
}
class MyState {
@observable items = [];
counter = new WithCounter();
refSet = new WithWeakSetRefs();
someLogic = (target) => {
if (this.refSet.set.has(target)) alert('alarm');
}
}
Для меня одна из причин ухода от redux и saga было сложность понимания и очень много однотипного кода, который усложнял как понимание, так и сопровождение. И если ты один на проекте, то еще куда ни шло. Но когда с тобой работают еще люди — это выливается в проблему.
К сожалению, я не нашел нормального решения. Поэтому был уход сначала в MobX, а потом в сторону личного решения, которое работает только для одного проекта. Да, не универсальный нож на все случаи жизни. Но зато порог входа простой и понятный для джуниоров.
Что бы порекомендовал — просто не усложнять. Или посмотреть в сторону более простых решений. Каких, не буду рекомендовать, что бы не разводить холивар.
Вся логика выносится в service layer. Это наша бизнес логика приложения. Запросы к серверу, обработка данных, кеширование итд. Все это находится в этом слое. Обычно у меня этот слой не зависит от какий либо фреймворков.
Далее, ппределяются данные, которые будут использоваться в двух местах одновременно. Как пример — профиль пользователя. И если посмотреть, то таких данных действительно очень мало и нет никакого смысла держать и в каком либо сторе. Проще и понятнее обратится к API service layer и взять данные когда они нужны.
Для данных которые будут использоваться в двух местах одновременно — уже выбрать стор. Это может быть redux/mobx/свое решение. У меня в проекте для этого свой велосипед. Маленькое и простое решение, которое покрывает всю необходимую функциональность. И ради трех/четырех пропертей тянуть в проект redux/mobx просто нет смысла.
Сейчас проверил, что данных в сторе всего три объекта.
Кстати, есть еще вот такой https://github.com/diegohaz/constate способ обойтись React Context API для не очень сложной логики. В двух словах: оборачиваем custom Hook в Context.
Бонусом идет bottom-up проектирование:
- пишем логику непосредственно в компоненте,
- стало сложно — оборачиваем в custom Hook
- стало нужно в двух местах — оборачиваем custom Hook в Context.
При этом объем рефакторинга минимальный. Вес самой либы 0.5 Kb
Надеюсь я такое «чудо» не увижу никогда) Рефакторить потом такое, это прям ржавым ножом в самую душу)
Ну прелесть этого подхода в том, что он не вносит нового API. Просто расшаривает состояние custom Hook на поддерево компонентов.
Нравится нам это или нет, но в React по сути придумали свою систему реактивности,
- с атомами:
useState()
—@observable
, - производными:
useMemo()
—@computed
, - и эффектами:
useEffect()
—reaction()
.
Часто этого бывает достаточно.
P.S. Против MobX ничего не имею. Сам начинал с Knockout еще, и был очень рад, когда кто-то реализовал подобную концепцию для React.
Проблема в том, что видение авторов React отличается от авторов MobX. И такое чувство, что первые пытаются усложнить жизнь вторым.
Например, если раньше можно было просто навесить @observer
на класс, то теперь с хуками встречаются вот такие чудеса: useObservableSource, потому что props
не реактивны.
Все это печально, и отпугивает новичков от MobX. Поэтому он и не так популярен, как мог бы.
кто после вас будет работать с этим «замечательным» кодом
Ну на MobX я видел не менее «замечательный» код, где reaction на reaction-е и subscribe-ом погоняет. Хотя многое можно было запихнуть в @computed
. Это все говорит о том, что инструментом нужно еще уметь пользоваться. А не «вот вам MobX и будет счастье».
Так ведь и сейчас можно взять и навесить @observer
на класс...
Вот где и правда разработчики React усложняют жизнь — так это вот тут: https://github.com/facebook/react/issues/15317
Одно из преимуществ использования хуков для управление состоянием — возможность идти в ногу с экосистемой реакта. Можно использовать популярные хуки вроде useMedia, useLocation, useQuery вне компонента, лучше отделяя логику от UI. Другие стейт-менеджеры предлагают писать свои обёртки вместо использования готовых хуков, так как готовые хуки не дружат с их системой реактивности (пример для Effector).
Другой вариант — в mobx-react-lite есть хук useLocalStore, который позволяет совмещать хуки с удобством Mobx (иммутабельность, точечные обновления UI, вычисляемые значения).
есть блок полей связанных, например личная информация о пользователях, с бэка приходят возможные наборы, взятые из баз партнеров. Эти блоки могут не содержать часть полей, необходимых нам, так же некоторые данные этих полей могут оказаться «плохими».
Задача: узнать у пользователя какие данные его, если нет его данных предоставить все поля для заполнения. Если узнали какой блок данных пользователя, показать пользователю только те поля, которых у нас нет, а так же поля, данные которых «плохие». Так же могут меняться типы поле и подписи к ним, в зависимости от количества пришедших блоков данных.
Если остановиться на createReducer
и createAction
, может, и не сильно меньше. А вот createSlice
прям прилично сократил бойлерплейт и порезал несколько папкофайлов. Значительно сократился объём исходников, даже общий объём приложения уменьшился (не смотря на подключение, по сути, дополнительной либы).
А еще rematch, easy-peasy, симбиоты тут ниже в треде проскакивали, тысячи их…
И на каждом проекте своя байда. А голый Redux неюзабелен. Тем и бесит =(
Лучшее средство для упрощения redux-кода, которое я встречал: redux-symbiote. Для асинхронных действий там тоже есть решение, но оно не использует redux-saga.
MobX это не всегда решение. Например, он с трудом натягивается на приложения с возможностью отменять и повторять действия.
Я поставил минус за вставку кода скриншотами. Такой код нельзя скопировать и трудно читать на маленьких экранах. Ознакомьтесь, пожалуйста, с возможностями разметки статей на Хабрахабре.
Как раз в Redux путешествие по истории реализовано максимально неэффективно — запоминаются N последних состояний, вместо того, чтобы запоминать только разницы между состояниями. Представьте, что если бы Git хранил коммиты не как diff'ы от начального состояния, а как промежуточные состояния с большим количеством дублирования. Это именно то, что предлагает документация Redux: redux.js.org/recipes/implementing-undo-history#second-attempt-writing-a-reducer-enhancer
В Mobx State Tree напротив можно хранить историю патчей состояния, а не сами состояния: mobx-state-tree.js.org/concepts/patches
Именно Mobx State Tree я подразумевал под натягиванием. Я в одно время, вдохновившись рассказами про MobX, провёл детальное сравнение Redux и MobX в контексте использования в моём приложении, и пришёл к выводу, что Redux подходит лучше. К сожалению, я не записал данные исследования и забыл подробности.
Как раз в Redux путешествие по истории реализовано максимально неэффективно — запоминаются N последних состояний, вместо того, чтобы запоминать только разницы между состояниями.
Не правда. Когда вы «копируете» состояние приложения, например, таким кодом:
const newState = {
...oldState,
foo: {
...oldState.foo,
bar: 'new bar',
},
};
Браузер не копирует все объекты, на которые ссылается oldState
. Все атрибуты newState
, кроме foo
, будут ссылаться на те же самые экземпляры объектов, что oldState
. Аналогично с атрибутами newState.foo
. В память добавятся только те части состояния, которые изменились. Это менее эффективно, чем хранить патчи, когда объекты имеют много атрибутов, но в реальных проектах это малая толика от общего вклада в производительность.
запоминаются N последних состояний
Вы, главное, учтите, что ввиду переиспользования одних и тех же объектов в этих состояниях, это получается довольно дешёвой и удобной операцией. Т.е. расход памяти примерно тот же самый, но без патчей.
Ох мы намаялись на прошлой работе с этими симбиотами, когда человек притащил их на проект, а потом уволился.
просто оставлю ето здесь https://redux-toolkit.js.org/
const result: Either<ApiError, Payment> = yield api(...);
А есть рациональный ответ на вопрос: "Зачем примеры кода делать картинкой?"?
Библиотеки react-redux и redux-saga просты, гибки и удобныЭто ваше мнение или откуда-то скопировано?) Не первый раз уже встречаю подобные высказывания в начале статей о redux.
Какое удобство на каждый чих создавать или менять по 10 файлов, или искать ошибку в 10 файлах?
Какая простота, когда на каждый запрос данных для компонента нужны, грубо говоря, код в компоненте, selector, action, action type, api, saga, reducer?
Вы не сделали последний шаг — надо убрать actions/actionsCreators и вместо них вызывать на сторе непосредственно ф-и из редьюсера.
Тогда теряется "главная" фишка redux-а: pub/sub. Когда на 1 action может реагировать сколько надо reducer-ов. Правда далеко не всем эта фишка нужна.
Думаю это для тесносплетённых сущностей, которые фигурируют сразу везде. Скажем может пригодиться в социальных сетях. Когда клик по кнопке в одном месте должен автоматически отразиться во всех остальных, т.к. переиспользуется одна сущность.
Мы вот как раз пишем соц. сеть, но этой фишкой не пользуемся. Мы всё свели к паттерну "команда". И теперь столкнулись с тем, что обеспечить нормальную нормализацию и последующую мемоизацию бывает очень и очень сложно. В случае pub/sub можно было бы просто тупо понавешать reducer-ов на типовые action-ы и менять сущности не централизованно, а децентрализованно. Но там тоже свои проблемы.
А вообще pub/sub имхо это больше для всяких сильно связанных систем с плагинами, вроде того же babel-я. Где куча всяких хуков и не стабильная структура кода (привет плагины для плагинов для плагинов). Но это не про redux.
Когда клик по кнопке в одном месте должен автоматически отразиться во всех остальных, т.к. переиспользуется одна сущность.Но он ведь и так отразится везде, где используется эта модель.
Это случится в одном из двух случаев:
- У вас всё нормализовано. Тогда не нужен pub/sub, но есть другие проблемы.
- У вас всё денормализовано. Тогда нужен pub/sub, чтобы обновления доехали до всех мест.
Оба варианта паршивые, т.к. имеют ряд проблем. У нас 1-ый. И мы уже "приехали" в то, что если сервер возвращает сущности в разном виде с разным перечнем полей, то обеспечить вменяемую нормализацию, скажем так, не тривиально. Особенно на уже большой кодовой базе.
У 2-го подхода другие проблемы.
Боюсь в двух словах разные подходы к работы с единым стором я описать не могу. Поверь, там не всё так просто. Ни с нормализацией, ни с денормализацией. Когда приложение большое, живое, и в нём есть много разных срезов одних и тех же сущностей, получаемых из разных API в разное время и это всё должно быть между собой согласовано, то ты неизбежно придумываешь какую-нибудь муть.
Сам redux тут особо не причём, это касается и MobX, и Vue, и KnockoutJS и чего угодно ещё. Ибо проблема этажом выше. Computed тут совсем не причём, т.к. речь идёт не о калькуляции чего-то из чего-то.
Вероятно верно и то и другое. Для этого нужны условия.
Дано:
- есть множество разрозненных, не связанных с друг другом, элементов на странице
- они отчасти питаются из одного источника данных (далее "реестр")
- они не зависимо друг от друга обновляют этот реестр общих данных
- они обновляют его разными срезами одной и той же информации (т.к. это не одна модель на одну сущность, а сколько угодно бакенду моделей, где разные наборы полей)
- всё это должно быть реактивным
- все корневые модели (скажем: пост, пользователь и пр.) довольно тесно связаны (например у поста сложная ветвистая структура где в нескольких местах могут быть указаны данные других сущностей, например автор поста)
- это всё должно работать быстро
Мы используем стандартный подход для redux — нормализация данных. Т.е. мы не храним одни и те же вещи в разных местах. Точнее очень стараемся не хранить, но не везде руки дошли сделать правильно. Стало быть мы точечно обновляет общий реестр данных. Разными наборами данных (там хитрый merge для каждой сущности). Каждый подписчик на нужные сущности реагирует на изменение самой колллекции и не только пытается выдрать оттуда нужные сущности, но ещё и вынужден проверять а хватает ли ему полей. Может его пак данных ещё по сети идёт, а те что уже есть от других компонент приложения недостаточны. При этом если он загружает список то там может быть так, что по одним сущностям хватает, а по другим нет. Ввиду разных пересечений разных компонент по моделям. Да и сами поля могут быть неравнозначны (это отдельный геморрой).
Т.к. это redux и immutability то нужно ещё это всё дело уметь выгружать из реестра. Да заодно выгружать не крупицами а сразу в едином обогощённом виде. Иногда сразу сотнями. Т.е. выгружать, скажем, данные поста сразу со всеми вложенными в него сущностями, а не лезть за каждой в стор отдельно. А это приводит к довольно сложной мемоизации (в меру сложной).
А ещё в виду той же иммутабельности и всяких уже браузерных sideeffect-ов хочется избегать лишних ререндеров, когда например были получены с сервера данные о посте пользователя, разумеется вместе с данными автора. И загрузили мы 10 постов, от 2 авторов. И важно почём зря не обновить авторов в реестре, если у них ничего не изменилось ничего, чтобы не вызвать лишние ререндеры в тех местах, где это вроде и не нужно (не все UI компоненты простые, всякие там Masonry таблицы бывают чувствительными к обновлению данных, плюс всякие скроллы и useEffect-ы).
Это далеко не всё. Это только то что сразу в голову пришло. На самом деле геморроя много. Часть из этого продиктовано иммутабельностью (но с мутабельностью проблемы были бы просто другими, в других местах), а часть (большая) нормализацией и тем как данные приходят с сервера.
В противовес всему этому можно было тупо забить на нормализацию. Хранить везде всё как взбренится. И просто отлавливать повсеместно только те события которые важно отловить и обновить нужные наборы данных уже поместу. Скажем есть пользователь "Вася", и вы за-follow-или его. Надо чтобы аватарка Васи теперь была с нужной рамкой везде, т.к. Вася теперь не просто Вася, а ваш кореш. Так что можно просто тупо отправить событие в стиле "изменить_статус_follow-ности для пользователя Вася", а все кому надо поймают и свои модели как им угодно поправят. Или не поправят, если им плевать. Вот это будет pub/sub. Какие-то вещи в нём делаются проще.
У нас есть следующие сущности в приложении:
1. Статьи
2. Комментарии
3. Пользователи
У каждой статьи и комментария есть автор (пользователь). У каждой статьи — список комментариев
Допустим, у нас есть такая структура. В каком именно месте вы стыкаетесь с проблемой? Какая именно связь является сомнительной?
Пока то, что вы описываете выглядит как проблемы именно кривости редакса.
Мы сталкиваемся с проблемами:
- как положить данные в стор (их нужно провалидировать, конвертировать, разобрать на составные части и положить в стор раздельно)
- данные о статье могут быть частичными, а могут и полными, может вообще только 1 поле придти
- данные о статье в разных частях приложения будут частичными очень по-разному
- некоторые наборы данных с бакенда очень сложно между собой согласовать
- потребители этих данных (компоненты) не хотят собирать эти паззлы по частям, хотят удобоваримый цельный кусок
- и не хотят чтобы сборка этого куска приводила тем или иным тормозам, вроде лишних рендеров, сбросов скроллов, всякой UI нечисти
- и не хотят чтобы были баги уровня — данных не хватило
- и не хотят чтобы были race-conditions
- и не хотят чтобы кто-то удалил данные зазря, когда кто-то их ещё использует
- и не хотят чтобы данные жили вечно (у юзера памяти не хватит если на длительное время оставить открытой вкладку)
- а потребителей масса, и в каждом могут быть свои баги
Redux тут только отчасти причём, ввиду того что навязывает нормализацию и иммутабельность. По сути будь тут что Vuex, что Redux суть примерно одна. Одни и те же проблемы. Полагаю что и MobX State Tree не выделится примерно ничем. Они этажом ниже. "Разруха, она ведь в головах".
Если на языке DDD, то, по-моему, речь идёт о ситуации, когда одна сущность предметной области маппится на разные "классы" в разных ограниченных контекстах.
И это тоже, да. Причём в одном могут быть подробные данные об аватарке пользователя в 100500 полей, а в другом только 1 поле url: string. И как их согласовывать? "Как-то"… По сути многие вещи решаются "как-то". Это печалит.
То есть проблема у вас в том, что хреновый АПИ, но при этом нужно как-то на клиенте синхронизировать все варианты?
Не приводить всё к одному типу в модели?
Ну тогда вам нужен pub/sub. Вам ведь нужно как-то распространить все обновлениям по всем местам? С этого разговор и начался. Либо вы приводите всё к одному формату и храните в одном месте. Это нормализация. Либо не делаете этого и храните везде по отдельности всё. Куда пришло там и лежит в том же виде. Это денормализация. Чтобы заставить оба подхода обновлять соответствующие данные ВЕЗДЕ вам придётся изрядно попотеть.
Касательно API, если у вас High-Load, то не backend пляшет от ваших хотелок, а вы от него. Да и не всегда сподручно ради одного-двух полей тащить громоздкие сущности в списках.
Ну тогда вам нужен pub/subНу допустим. И в чем проблема?
Касательно API, если у вас High-Load, то не backend пляшет от ваших хотелок, а вы от него. Да и не всегда сподручно ради одного-двух полей тащить громоздкие сущности в списках.
Обычно нужен баланс. Это не значит, что если у нас хайлоад, то один раз сервер присылает
{ user_id: 123 }
, а второй раз: { user: { id: 123 } }
. Таких соглашений достаточно, чтобы иметь адекватный и предсказуемый апи.Ну допустим. И в чем проблема?
Я окончательно потерял нить беседы. Ты хотел чтобы я показал где может быть полезным pub/sub — я показал. Ты спросил в чём могут быть сложности без него — я написал. Причём достаточно подробно.
Что ещё осталось непонятным? :) Про сервер не готов дискутировать. Во всяком случае здесь это уже будет совсем флейм.
Тогда теряется «главная» фишка redux-а: pub/sub. Когда на 1 action может реагировать сколько надо reducer-ов. Правда далеко не всем эта фишка нужна.
Я не совсем понимаю. Вот у нас есть нормализованные данные. Допустим, в разных местах приходят в разном формате. Как только приходят — они сразу нормализуются и обновляются одним экшном и одним редьюсером. Не вижу смысла тащить грязные данные дальше, чем в этом есть смысл.
они сразу нормализуются и обновляются одним экшном и одним редьюсером
Если ты это делаешь, то pub/sub не нужен. Мы как раз так и делаем. У нас нет pub/sub. Но это создаёт ряд проблем.
Вот если ты забьёшь на нормализацию. И не будешь ничего ни во что конвертировать. Просто пришёл ответ с сервера, ты в ближайший участок стора его положил. Положил как есть, не приводя к какой-то единой форме. Вот в этом случае тебе нужно уметь разными редьюсерами слушать некие генеральные типы action-ов, которые будут распространять все возможные изменения моделей вниз по древу от корня. И все кому надо — на них отреагируют и нужным образом изменят свои данные. Вот это будет pub/sub. Альтернативный подход.
В общем pub/sub это обыкновенная подписка на изменения или события. Streams, Events, Computed — всё это примерно об одном и том же в разной форме.
Тогда теряется "главная" фишка redux-а: pub/sub. Когда на 1 action может реагировать сколько надо reducer-ов.
Вообще это антипаттерн. Несколько редьюсеров на один экшон нарушают принцип single source of thuth. Если надо обрабатывать один экшон в разных местах — стоит сделать рефакторинг.
Тем не менее это одна из главных заповедей redux ;-) А вообще подобным образом много что в IT устроено. Та же самая Apache Kafka (поправьте если я не прав) столь популярная на бакенде.
А при чем тут сам pub/sub? Мы про конкретную реализацию т.е. редакс. А идеология редакса, по сути, прямо запрещает обработку одного экшона в разных редьюсерах.
А идеология редакса, по сути, прямо запрещает обработку одного экшона в разных редьюсерах.
можно ссылочку на это? (всё наоборот)
А при чем тут сам pub/sub?
В основе Redux лежит принцип pub/sub. Просто это не так очевидно на первый взгляд, потому что это не просто EventEmitter.
Если бы идеология редакса запрещала обработку одного экшона в разных редьюсерах — у него была бы совершенно другая архитектура.
Если бы идеология редакса запрещала обработку одного экшона в разных редьюсерах — у него была бы совершенно другая архитектура.
Вы сейчас исходите из того, что редакс написал разумный человек. Это не так.
В итоге с одной стороны идеология редакса это запрещает, но другой стороны — архитектура редакса сделана ровно так, чтобы это поощрять.
redux.js.org/faq/actions#is-there-always-a-one-to-one-mapping-between-reducers-and-actions
К сожалению, всё не так. Идеология redux прямо предполагает, что на один action реагируют много reducer
Да, немного поправлюсь. Один экшон должен обновлять только один кусок состояния. Но этот кусок состояния может обновляться, конечно, несколькими редьюсерами — декомпозиция же.
Один экшон должен обновлять только один кусок состояния
Поделитесь, пожалуйста, источником этой информации. Где вы такое вычитали то? Там же где и то, что бизнес-логика должна быть в actionCreator-ах? :)
Поделитесь, пожалуйста, источником этой информации.
Это single source of truth. Если в храните одну и ту же информацию в разных местах — то данный принцип нарушен.
Дык, загвоздка то в том, что обработка одного и того же сигнала в разных местах, никак не противоречит концепции нормализации (single source of truth). Сигнал это не обязательно "пришли новые данные, их надо расфасовать". Это может быть всё что душе угодно. И связи там могут быть вовсе не 1к1.
С примером не помогу, т.к. не использую множественную обработку одного экшна и не приветствую (хотя таких как я минимум, я против течения, так сказать). Но думаю, если вы кинете клич, то среди фанатов redux вам десяток другой соберут.
Дык, загвоздка то в том, что обработка одного и того же сигнала в разных местах, никак не противоречит концепции нормализации (single source of truth)
Противоречит, конечно. Если вы обработали один сигнал в разных местах — то информация об этом сигнале теперь есть в разных местах.
Можно единственный пример привести, когда все нормально — это если в вашем сигнале по факту внутри пейлоада приходит два несвязанных куска данных и вот один кусок вы обрабатываете в одном месте, а другой — в другом. В этом случае, по факту, реакция будет на разные данные, да. Но тогда непонятно, почему нельзя разделить этот сигнал на два разных.
Action-ы необязательно содержат данные. Это могут быть "призывы к действию". Даже само название об этом говорит.
Action-ы необязательно содержат данные. Это могут быть "призывы к действию"
Соответственно, у вас в двух разных местах хранится информация об одном и том же "призыве к действию", что сути не меняет. Но это в любом случае словесная эквилибристика, как экшоны называть. По факту экшон — это всегда данные :)
Потому что нужна некая транзакционность
В указанном примере транзакционность нарушена не будет.
В одной ветке стора мы на сигнал "user logged in(user_id)" обновляем аутентификационную и авторизационную информацию по user_id для внутреннего использования, в другой, например, загружаем список новых сообщений для непосредственного отображения.
Ну так это должны быть разные экшоны. Один — залогинить юзера, другой — загрузить список сообщений
Ну так это должны быть разные экшоны. Один — залогинить юзера, другой — загрузить список сообщений
Юзер уже залогинен, мы получили ответ сервера об этом и хотим чтобы стор на это отреагрировал. Почему мы должны разбивать посылать одни и те же данные стору два раза?
Соответственно, у вас в двух разных местах хранится информация об одном и том же "призыве к действию", что сути не меняет
Информация о призыве к действию вообще нигде не хранится. Ни до редьюсера, ни после.
почему нельзя разделить этот сигнал на два разных
Потому что нужна некая транзакционность, иначе можно получить рендер в невалидном состоянии: например обращение к null
в одном из селекторов.
Да, это можно решить и по-другому (напр. redux-batched-actions), но ванильный Redux ничего не говорит об этом.
Не противоречит, потому что single source of truth в redux — это общее состояние стора, а не actions. В одной ветке стора мы на сигнал "user logged in(user_id)" обновляем аутентификационную и авторизационную информацию по user_id для внутреннего использования, в другой, например, загружаем список новых сообщений для непосредственного отображения. Если всё это хранить в одном месте, то это точно будет антипаттерн god object
Например у меня в проекте есть action `searchAll({ searchText })`, на который реагирует saga для запуска запроса, сбрасываются фильтры, устанавливаются правильные флажки для отображения доступных фильтров. В целом, можно было обойтись одной ветвью стейта и редьюсером (что я, наверное, и сделал бы), но я попал на проект, где вместо одного searchAll диспатчился десяток разнородных action, что тригерило лишние вызовы хуков, редьюсеров и так далее.
Вы сейчас исходите из того, что редакс написал разумный человек. Это не так.
Верно подмечено! Жаль пока что не все это понимают.
Вы сейчас исходите из того, что редакс написал разумный человек. Это не так.
Верно подмечено! Жаль пока что не все это понимают.
Как уменьшить количество и увеличить читаемость кода в react-redux, redux-saga