Comments 55
Вот:
codesandbox.io/s/billowing-meadow-ldme1?file=/src/App.tsx
Мне кажется, вы код таких разработчиков, как Роберт Мартин или Мартин Фаулер, тоже назовете лапшой)
Свои усложнения я как раз постарался обосновать в 2-х статьях. Постарался объяснить, в каком-направлении двигаться, чему следовать.
Вы думаете в вашем примере с компонентом и стором достаточно информации, чтобы сделать проект? Что в итоге получится? На 50 страниц будет 50 сторов по 1000+ строчек в каждом сторе или что-то другое?
Напишите свою статью с примерами и объяснениями. Тогда под каждой статьей не нужно будет писать, что в очередной статье лапша.
А вам нравится предложенная архитектура?
Нет, то, что в этой статье мне не нравится от слова совсем. Подход усложнять все элементарное на ровном месте и делать все сразу максимально универсальным и на все случаи жизни я призираю. Более того автор ссылается на каких-то других разработчиков(можно например посмотреть в комментариях выше) как на пример для подражания — это вообще смешно и абсурдно, ведь есть же своя голова на плечах.
Меня удивило что вы говорите " с Redux-thunk жить как-то проще.".
const state: LocalState = useContext(LocalStateContext)
hendleAddMachineHistory(state.machine[state.history_pos])
не делали два элемента в массиве истории ссылками на один объект.Это называется не "целостность", а как-то по-другому.
Можете скинуть ссылку, где эта фича описывается в Redux-thunk?
Мне кажется вы что-то путаете. В redux из коробки нет многих необходимых фич, и с его плагинами по отдельности та же ситуация.
Ссылки нету, но сейчас проверю как оно в редакс работает
import { cloneDepp } from 'lodash';
import { toJS} from 'mobx';
lalala.push(cloneDeep(toJS(yourState)));
// Или
function pushToHistory(state) {
lalala.push(cloneDeep(toJS(state)));
}
pushToHistory(yourState);
Вы запушите в массив полную честную копию состояния
const { todoListStore } = useContext(StoresContext);
const { todoController } = useContext(ControllersContext);
Как уже писал ранее, для каждой сущности мне больше нравится отдельный хук
const todoListStore = useTodoList();
const todoController = useTodoController();
Вьюха просто получает для своих нужд реализацию того или иного интерфейса, при этом не раскрывая явно её источник или жизненный цикл.
Что делать, когда наблюдаемые данные одного стора зависят от наблюдаемых данных другого стора и происходит обновление первого стора?
Я стараюсь избегать таких цепочек обновлений и выношу вычисления в контроллер. То есть
из сторов считываю необходимые данные, обрабатываю их, и затем передаю их сторам, использующим эти связанные данные.
Правильно ли я понял, что в контроллере ставится autorun на наблюдаемые данные другого стора, и в это авторане меняются данные первого стора, вместо того чтобы канонично сделать в первом сторе свойство computed или просто свойство?
Хинт: вместо
export default class BaseController {
private _mainStore: BaseStoreType;
constructor(mainStore: BaseStoreType) {
this._mainStore = mainStore;
}
можно
export default class BaseController {
constructor(private _mainStore: BaseStoreType) {}
Как уже писал ранее, для каждой сущности мне больше нравится отдельный хукЯ пока достаточно глубоко не размышлял над этим моментом. А вариант с контекстом мне самому не нравится. Но в этом примере решил с ним попробовать.
Правильно ли я понял, что в контроллере ставится autorun на наблюдаемые данные другого стора, и в это авторане меняются данные первого стора, вместо того чтобы канонично сделать в первом сторе свойство computed или просто свойство?Нет. Я имел ввиду, что я бы постарался избежать наблюдение одним стором за изменениями в другом сторе и попытался бы решить эту ситуацию в контроллере как-то так:
someControllerAction(newData) {
const dataFromStoreA = storeA.getSomeData();
const newDataForStoreA = { ...updatedData, ...newData };
const newDataForStoreB = someTransforms(newDataForStoreA);
storeA.updateSomeData(newDataForStoreA);
storeB.updateSomeData(newDataForStoreB);
}
За хинт — спасибо! Видел такое раньше, но забыл.
А вариант с контекстом мне самому не нравится.
В самом контексте ничего плохого нет, наоборот (имхо) только там и надо хранить постоянно живущие модели. Просто вот эту работу с контекстом можно запрятать в хуки-однострочники.
Нет. Я имел ввиду, что я бы постарался избежать наблюдение одним стором за изменениями в другом сторе и попытался бы решить эту ситуацию в контроллере как-то так
У этого подхода есть минус. Вот, к примеру, имеем storeA и один или несколько контроллеров к нему, по ситуации. Ещё есть storeB, со своими контроллерами. В какой-то момент для некой дополнительной надобности запилили storeC, в котором что-то зависит от storeA и storeB. И теперь, вместо того чтобы в этом новом сторе добавить компутеды со ссылкой на storeA и storeB, просто расширив систему, ищем все контроллеры и вообще все куски кода, где прямо или косвенно обновляются вышеуказанные сторы, определяем, повлияют ли эти изменения на storeC, и везде дописываем логику обновления. Сделали сторы независимыми, пожертвовав всеми плюшками реактивного подхода.
ищем все контроллеры и вообще все куски кода, где прямо или косвенно обновляются вышеуказанные сторы, определяем, повлияют ли эти изменения на storeC, и везде дописываем логику обновления.Что-то вы перебарщиваете. Не нужно смотреть на другие обновление. Надо только получить сохраненные значения из сторов. И надо будет написать ровно одну функцию в контроллере. То есть одна новая зависимость от нескольких сторов, это одна новая функция в контроллере.
На картинке в левой части я привел ситуацию, когда обновления сторов зависят друг от друга и какую ситуацию я предпочел бы избежать. Правее — то, что я описывал, то есть через контроллеры. Надеюсь, понятно нарисовано.
В вашем случае при дебаге надо просмотреть все связанные сторы, в моем варианте найти действие контроллера, которое отвечает за конкретное обновление группы сторов.
Мне кажется, если проект такой, что между сторами много зависимостей, то в моем варианте будет проще разобраться. Если не много зависимостей, описанный вами вариант будет лучше.
И надо будет написать ровно одну функцию в контроллере. То есть одна новая зависимость от нескольких сторов, это одна новая функция в контроллере.
Увы, но нет. Возвращаясь к моему примеру: если у нас, допустим, storeC.value = storeA.value + storeB.value, и значения сторов А и В меняются в разных controller action (вполне ведь возможный кейс?), то во всех этих экшенах надо будет дополнительно вручную обновить storeC.value. Представьте себе комбинаторную лавину таких коррекций и их сопровождение, если сторов и связей много.
Картинка слева выглядит угрожающе только из-за циклических зависимостей. Их в большинстве случаев можно разрулить, если правильно декомпозировать. "Знакомя" сторы между собой через DI в конструкторе и не меняя наблюдаемые поля в реакциях, избегаем цикличности.
А в чем принципиальная разница между этим решением и использованием:
- redux или useReducer для хранения данных
- thunk-middleware или обычных функций для логики контроллера
Кажется, что с точки зрения "слоев" и отношений между ними полный паритет. Разница только в синтаксисе, но бороться с бойлерплэйтом redux научились уже давно.
Принципиальная разница тут в том, что:
- mobx умеет точечно обновлять затронутые компоненты, в то время как redux умеет только проходиться по всем подпискам в цикле;
- проще изначально взять решение без бойлерплейта, чем брать решение с бойлерплейтом, а потом с ним бороться.
Кажется, что с точки зрения "слоев" и отношений между ними полный паритет.
Рад что вы признали это, потому что обычно аргумент номер 1 против mobx звучит как "mobx не даёт возможности разделить слои или организовать односторонний поток данных".
Не то чтобы я защищал redux или нападал на mobx. Но в контексте статьи мне непонятно причем здесь redux и mobx.
Звучит, как будто бы какой бы инструмент мы не выбрали, принципиальной разницы для архитектуры нет. А значит можно отвязать весь топик и от redux и от mobx и говорить просто о принципах хорошей (по каким-то показателям) архитектуры, не спекулируя о субьективных минусах и плюсах конкретных инструментов.
Если вы почитаете статьи про redux или mobx на этом сайте, то увидите кучу комментаторов, которые не осилили самостоятельное построение redux-подобной архитектуры средствами mobx, из чего сделали вывод будто бы достичь хорошей (с их точки зрения) архитектуры на mobx невозможно.
Насколько я понимаю, данный пост призван продемонстировать обратное, что довольно трудно сделать без упоминания mobx или архитектуры redux.
Отвязать от redux и от mobx было бы сложно, т.к. проще показать на примере популярных и знакомых многим решениях. К тому же хотелось сделать акцент на составляющих Redux, чтобы задумывались об их назначении, действительно ли стоит писать именно так или за специфичными вещами скрываются более простые.
Здорово, что в Redux спустя несколько лет более-менее решена проблема бойлерплейта. Я же постарался показать, что, поразбиравшись с назначением составляющих Redux и взяв другой достаточно гибкий инструмент, можно было получить похожее (с некоторыми субъективными улучшениями) решение, не создавая эту проблему в прошлом.
Я же постарался показать, что, поразбиравшись с назначением составляющих Redux и взяв другой достаточно гибкий инструмент, можно было получить похожее
Зачем??? Зачем что-то похожее на это ущербное недоразумение? Тем более статью тут писать и показывать такой код. Фишка MobX в том, что можно и нужно писать человеческий код, а не дичь. Но вы к сожалению используете MobX не по назначению судя по коду, который вы пишете. И вам пожалуй стоит оставаться в мире Redux и его поделок, до MobX'a рано ещё видать.
Заворачивание глобальных стейтов в контекст провайдеры, использование никому не нужных абстракций для стейтов, отдельные от стора контроллеры на кой-то черт, нагромождения лапше кода. Я не понимаю откуда тяга вот к этому всему, если все можно делать намного проще и писать намного меньше кода.
Зачем???Затем, чтобы побольше людей задумалось: «а может не стоит писать эти редьюсеры, dispatсh-и?».
И вам пожалуй стоит оставаться в мире Redux и его поделокВнезапно, я ни в одном своем проекте не использовал Redux, отказавшись от него с первой недели знакомства. Но приходилось пару раз работать с ним в чужих проектах.
Уже все React разработчики на хабре знают, что есть только один правильный вариант написания кода — тот, которым вы пользуетесь. Несомненно, вы лучше всех знаете, как использовать MobX по назначению)
Зачем?
Если много людей пользуются redux и жалуются, что (цитата) "mobx не предписывает им никакой архитектуры, из-за чего в коде образуется лапша" — значит, надо показать как привычная им архитектура на mobx делается. А дальше пусть уже сами решают где и чего там можно упростить.
Заворачивание глобальных стейтов в контекст провайдеры
…необходимо для SSR и тестов.
необходимо для SSR и тестов.
Если SSR вот прям действительно обязательно нужен, то github.com/puppeteer/puppeteer с кэшированием в помощь.
Тесты можно запускать в разных процессах, а не в одном. Тогда и экземпляры будут каждый раз новые.
Отсюда вывод, заворачивание глобальных стейтов в контекст провайдеры не нужно на самом деле. Код станет намного лучше.
Если много людей пользуются redux и жалуются, что (цитата) «mobx не предписывает им никакой архитектуры, из-за чего в коде образуется лапша»
Это лишь потому, что они пока не доросли до серьезного уровня и не умеют пока вообще в целом код писать хорошо и архитектурить, поэтому проблема только лишь в людях, а не в свободе MobX'a. Крайне глупо называть свободу минусом.
Чего? Запускать браузер на сервере в ответ на запрос с клиента ради того чтобы рендерить разметку?
Нахрена такие извращения, когда достаточно использовать внедрение зависимостей через контекст?
Чего? Запускать браузер на сервере в ответ на запрос с клиента ради того чтобы рендерить разметку?
Нет, не в ответ на запрос, страницы рендерятся в фоне и разметка кэшируется, а в ответ на запрос ты достаешь разметку из кэша и сразу же выплевываешь клиенту.
На выходе:
1) Клиент получает ответ максимально возможно быстро.
2) Во время разработки не надо думать о SSR и писать говнокод по типу заворачивания глобального стейта в контекст и т.п.
Отсюда, ни одного минуса, только жирные плюсы.
Говнокод — это то, что предлагаете вы. А внедрение зависимостей — обычная практика для повторно используемых частей программы.
А внедрение зависимостей — обычная практика для повторно используемых частей программы.
Это и есть обычный говнокод который предлагаете вы, любители лапши и любители загонять себя в рамки и ограничивать.
P.S. и у нас тут не программы, у нас современное front-end приложение, тут вам не 90ые годы. И код тут пишут по другому.
P.S. и у нас тут не программы, у нас современное front-end приложение, тут вам не 90ые годы. И код тут пишут по другому.
Современное front-end — это как раз "программа", и потому код лучше писать по бестпрактисам, а не как в 90е, с глобальными переменными и прочей анархией.
потому код лучше писать по бестпрактисам
С чего вы взяли что если кто-то что-то назвал бестпрактис, то это на самом деле бестпрактис? Ведь есть же своя голова на плечах, есть удобство написание кода, есть скорость написания кода, есть очевидность, есть простота, есть легкость с которой можно внести правки или добавить ту или иную фичу. А когда вы себя ограничиваете и загоняете в рамки, вы лишаетесь всего этого.
с глобальными переменными
А в чем проблема? У нас тут typescript, Это не просто переменная которая хрен пойми зачем и хрен пойми что делает и хрен пойми откуда берется, она импортируется из абсолютно конкретного места.
Обратиться к контексту — так же просто и легко, как и к глобальной переменной, от использования контекстов ничего не страдает.
А вот использование puppeteer для SSR я не могу назвать ни простым, ни лёгким, ни быстрым в написании.
Обратиться к контексту — так же просто и легко, как и к глобальной переменной, от использования контекстов ничего не страдает.
Вы можете обратится к контексту, только из реактовских конпонентов и из реактовских хуков. Вы не можете просто взять и обратится к глобальному хранилищу по другому, вот вам и ущербное ограничение. Вопрос нафига оно нужно? Если можно и нужно без него.
А вот использование puppeteer для SSR я не могу назвать ни простым, ни лёгким, ни быстрым в написании.
Это пишется 1 раз и всё. Более этого, это вообще не сложно. Далее работает универсально что для реакта, что для vue, что для svelte и т.п. Максимум что вы можете менять, это перечислять список урлов в специальном файлике, которые время от времени будут обновляться в кэше.
Вы не можете просто взять и обратится к глобальному хранилищу по другому
Можно через DI. Не к "глобальному хранилищу", а к тем сторам, которые требуются.
Вы не можете просто взять и обратится к глобальному хранилищу по другому, вот вам и ущербное ограничение.
Во-первых, при SSR и при тестировании UI в первую очередь именно обращение к хранилищу из реактовых компонентов и хуков и требуется. А другие части программы могут обращаться к хранилищу как угодно.
Во-вторых, для обращения не из UI куда угодно имеется внедрение зависимостей через конструктор, которое особенно удобно в TypeScript.
Писать «удобно», «быстро», «очевидно», «просто» и «легко» без следования данным принципам можно разве что решения простейших задач. Современные СПА к сожалению давно вышли за рамки «простейших задач». Это не относится к условным блогам и новостным лентам, конечно же, но для подобных вебсайтов и СПА будет излишеством.
Современное ФЕ комьюнити вырабатывает свои бест-практицес, к сожалению, не используя 20+ летние наработки из других технологий связанных с персистент клиентом, а наоборот «от решения задач интерактивности в контексте классических (ССР) веб приложений».
Отсюда большое количество метаний в разные стороны (флакс, редакс, хуки, css-in-js, js-in-css, yet-another-templating-engine). Особой пикантности этим толодвежиниям добавляет факт что часто трендсеттерами являются не опытные программисты, а эффективные ораторы. Из за чего експерементальный подход написанный талантливым практикантом становится мейнстримом на короткое время и подыхает в конвульсиях как только появляются реальные большие приложения, основанные на подобном подходе. Или не подыхает, а долго и грустно трепыхается на инвестициях от крупных технологических гигантов.
Каждый из подобных талантливых ораторов обязательно вещает про УБОПЛ(«удобно», «быстро», «очевидно», «просто» и «легко»). И это логично и правильно. Вот только УБОПЛ не берётся из вакуума и на больших кодовых базах не возможен без соблюдения определённой проектной дисциплины.
Перечисленные вами факторы и обеспечиваются соблюдением принципов солид
SOLID не про это. Это полная противоположность.
А вот KISS + DRY + YAGNI как раз про это.
DI это одна из техник, активно использующихся для разделения ответственностей, следующей из него сегрегации интерфейсов, а также инверсии зависимостей, необходимой во многих случаях для удовлетворения принципа Open/Closed
Вот вот, ущербная практика, лишний код, лишние нагромождения, лишь бы удовлетворить усложнения на ровном месте которые предписывает SOLID.
удалено. Промахнулся веткой
Компоненты вольны использовать сторы (как тутъ), сторы наделены геттерами и сеттерами. Если передо мною обьект и у него публичная операция на модификацию — ничего не сдерживает меня от использования этой операции.
Насколько я понимаю в вашем случае — «ничего кроме строгого запрета так не делать».
По моему опыту — такие «вербальные» запреты не работают.
Т.е. технически я не вижу причины отсутствия стрелочки «pass» направленной от вью к стору.
Поправьте меня если что-то пропустил.
Я не разделяю, т.к. у меня не возникало проблем на код ревью. Объяснил, пару раз поправил на код ревью, потом писали код в рамках заданной архитектуры и изредка спрашивали, если где-то были сомнения. К тому же, каждая страница в проекте является примером.
То есть в этой архитектуре надо соблюдать запрет изменения стора из компонента, а вот как соблюдать — это уже на усмотрение разработчика.
Поставил вам плюс, а то кто-то минусует видимо из-за того, что мнения не совпадают с его мнениями, что противоречит этикету хабра.
По сабжу: лично для меня такое соглашение (а скорее его важность) являлась бы шоустоппером для использования подобной структуры кода.
Я не фанат редакса (в какой то степени я его хейтер, причем ещё с тех времен когда это не было менстримом), но его применение возможно только при жестких програмных ограничениях. Косплеить редакс без этих ограничений, на мой взгляд, не имеет практического смысла. Вам повезло с командой (либо вы настолько сильный лид что смогли воспитать команду), мой же опыт не позволяет надеятся на соблюдение подобных соглашений в долгосрочной перспективе.
оффтоп: даже в редаксе эта задача не решена до конца и основывается на соглашении «ну не меняйте стейт, падлы, ну пажжжалуста». А ещё там не решена работа с асинхронностью — тханк это днище, которое делает из екшнов-посылок-данных «екшны-которые-могут-хранить-в-себе-подпрограмму-по-майну-биткоина». Т.е. вопросов редакс оставляет больше чем ответов, но будь добр компонент-екшнкриатор-екшн-редюсер-коннект напедаль для хелоуворлда.
Если же выкинуть из головы редакс в принципе и обсуждать предложенный подход к структурированию кода как автономный — разделение на контроллер и стор (и соответственно наличие двух точек взаимодействия для вью) мне кажется ненужным усложнением.
Получается что вью дёргают методы контроллеров, «рассчитывая» что изменяться данные в «сторе», т.е. присутствует неявная зависимость.
Мне кажется более естественным способом привязки вью к данным наличие классической вьюмодели. У неё вью дергает методы и читает из неё данные. При этом вьюмодели инкапсулируют данные как душе угодно и могут отдавать только ридонли слепки по необходимости.
Вынесение же логики по привязке к апи, работе с локал стораджем, авторизации, whatever, может осуществляться дополнительными сервисами, созданными для обеспечения данной ответственности.
Лично мой подход — внедрение подобных сервисов во вьюмодели следуя Dependency Injection, но это уже вкусовщина. Тутъ пример в котором используется ДАО, инкапсулирующее работу с источником данных.
Правда к использованиию реакта и мобкс я пришел с другой стороны: сначала начертил «какие елементы в структуре мне нужны» а после этого брал инструменты для реализации. Ну и по хорошему там должен быть преакт а не реакт, но для уменьшения кол-ва екзотики остановился на реакте.
кстати, я бы рекомендовал переоформить ваш екземпляр тудумвс так что-бы он в точности соответствовал стандартному (стили, локал сторедж вместо псевдо-апи, роутинг). Так гораздо легче сравнивать реализации лоб в лоб.
Видел очень мало проектов, в которых redux используется в чистом виде со всеми actions, action-creators и тд.
Есть великолепный redux-toolkit и прекрасный rematch которые этот бойлерплейт минимизирует настолько, что код становится меньше (и более читабельный) чем в том же мобх.
По скорости redux обходит. По компактности и понятности кода — тоже. По концепции (один
немутабельный стор) — также обходит.
Мобх чтобы избежать хаотических ререндеров всего дерева вообще каждый компонент окружает react memo, что создает дополнительную нагрузку.
И, кстати, ни в одной статье не видел нормального примера, как протаскивать мобх вниз? Неужто инклюдить стор в каждом компоненте? Или context? Огромный недостаток mobx — отсутсвие известного общепринятого паттерна применения.
В 9 из 10 крупных проектов (что-то больше чем todoList) в крупных компаниях будет redux — он предсказуем, быстр, читаем.
код становится меньше (и более читабельный) чем в том же мобх
Не верю.
По скорости redux обходит.
Не верю.
Мобх чтобы избежать хаотических ререндеров всего дерева вообще каждый компонент окружает react memo, что создает дополнительную нагрузку.
Нагрузку на что?
И, кстати, ни в одной статье не видел нормального примера, как протаскивать мобх вниз? Неужто инклюдить стор в каждом компоненте? Или context? Огромный недостаток mobx — отсутсвие известного общепринятого паттерна применения.
Вы, вообще-то, только что прочитали пост где на подобный вопрос отвечают!
По скорости redux обходит.
За счет чего? За счет пробегания каждого экшена по всем редьюсерам и всем селекторам, с иммутабельным пересозданием части дерева? Вы вообще в курсе, как оно работает?
И да, в вопросе перерисовок мобх тоже более точечный. Например, при некотором условии мы не рендерим кусок, в котором обращаемся к наблюдаемому полю, и как следствие не хотим за ним следить. В мобх это из коробки, а в редуксе, если надо так же, придется доработать напильником селектор, закопипастив в него условие.
код становится меньше (и более читабельный)
Особенно когда дело доходит до селекторов, создающих объектные структуры и требующих мемоизацию и простигосподи reselect (не к ночи будь упомянут). Редукс ведь любит нормализованные данные, а их иногда надо собирать в кучку. В этих случаях мутабельный мобиксовый подход, где observable.deep структурка уже в готовом виде, божит как никогда.
По концепции (один немутабельный стор) — также обходит.
Так себе концепция. Уж лучше изолированные (потенциально, прошу заметить, переиспользуемые) сторы, со своими методами обработки стейта, с инкапсуляцией и т.д. Которые между собой стыкуются через DI в конструкторе. Просто и очевидно, кто от кого зависит, кто с кем и как взаимодействует.
Неужто инклюдить стор в каждом компоненте? Или context?
Контекст. Редукс тоже там хранит данные. Да в общем-то как и все.
Огромный недостаток mobx — отсутсвие известного общепринятого паттерна применения.
MobX — это ООП. Так что паттернов, проверенных временем, полно. SOLID, DI, прочее… Правильный ООП рулит, что бы там ни говорили.
Реализация архитектуры Redux на MobX. Часть 2: «Пример на MobX»