Как стать автором
Обновить

Комментарии 48

Кому-то может быть полезным, но я вот вообще не понимаю, зачем нужен классовый DI в SPA и чем плохо глобальное хранилище. Парой строк оно расширяется и чистится


componentWillMount() { this.context.extendGlobalStore([NewStore]) }
componentWillUnmount() { this.context.clearGlobalStore([NewStore]) }
render() { return <>{this.context.store.newStore.param}</>}

Этот компонент без проблем выносится в отдельный чанк и подгружает компоненты, сторы, действия, апи и валидаторы (при необходимости). Можно оформить в декоратор или HOC, будет всего пара бойлерплейт-строк, в отличие от замороченных DI. Подмена логики для каждой платформы или устройства, локального, стендового или прод-окружения, подмена моков (не понимаю зачем, но раз уж кому-то надо) делается очень просто, если передавать через пропсы это все дело


<InjectStoreHoc newStores={[NewStore]} apiMocks={IS_LOCAL ? apiMocks : undefined} />

Если нужно вот это вот makeJuice, то эту функцию можно определить в глобальном сторе (хотя я не сторонник хранить экшены в сторах aka моделях) и вызывать с параметром, определяющим источник данных, а этот источник подгружать асинхронно по схеме выше


store.juicer.makeJuice(store.apples)
store.juicer.makeJuice(store.oranges)

// либо универсально, если через HOC передан FruitStore
store.juicer.makeJuice(store.fruit)

Сама функция makeJuice таким образом будет выглядеть как


@computedFn
makeJuice = (fruit: IFruit) => fruit.giveJuice()

И никаких дополнительных либ, презентация не на 98 страниц а на 2, обучение коллег занимает 10 минут, весь дополнительный код подгружается отдельным чанком, глобальный стор чистится и освобождается память при переходе на другую страницу, легко тестировать, передавая замоканный стор, имплементирующий IFruit. Не надо всей этой возни с бойлерплейтом, работать с подобным очень неудобно.


Единственная заморочка — в расширении типов глобального стора, так как при таком варианте TS не будет знать, что такое store.fruit, переданный через HOC, но решается либо обычным const fruit = store.fruit as IFruit, либо declare в классе, либо ручным расширением типов глобального стора, либо заведением в глобальном сторе public fruit: IFruit с пустым значением, либо типизированным пропсом — это все нужно только для статической проверки, поэтому выбрать можно по вкусу.


Код из репо комментить не буду, там слишком export default)

Сколько не пишу на React всё никак не пойму, зачем кто-то тащит в него DI. Что нам даёт DI? Может я чего-то недопонимаю?


  1. Упрощает тестирование: можно заменить 1 зависимость фейковый и сделать нужные проверки или реализовать в нём иное поведение
  2. Обобщает некоторые компоненты: возможность написать такой компонент, который можно задействовать в разных частях приложения, с разными зависимостями

Я ничего не забыл?


Самое простое — можно передавать всегда все зависимости через props. Каноничненько и явно, но тогда будет у вас props-hell на самых ранних стадиях. Ок, не вариант. Всё props-ми не сделаешь. Тот же i18n с корня проекта везде прокидывать — врагу не пожелаешь.


Остаётся контекст и глобалки. Глобалки зло, по ряду причин, где наиболее очевидная — возможна ситуация когда единовременно на странице есть потребители разных зависимостей одного типа. Ок, глобалки отпадают. Для внедрения глобалок вообще нужна серьёзная аргументация.


Контекст? Ну да. Это единственное, что позволяет сделать вам привязку к конкретному instance-у зависимости в конкретном месте (пусть даже всему поддреву).


Хорошо, у нас есть контекст. Он есть прямо из коробки. Нужны ли все эти бубны с inversify или можно просто взять контекст? DI в статье сделан как раз через контекст.


Вернусь к изначальному вопросу. Какую головную боль с нас снимает DI?


  1. Тесты. Тут мне кажется в 1000 раз удобнее просто мокнуть import-ы. Нет лишнего кода. Есть типизация. Никаких конструкторов, никаких классов, контейнеров, bind-ов, никаких DI. И главное всё очень просто. Я про всякие jest.spyOn и подобные штуки. Тащить в приложение DI только из-за тестов я бы точно не стал.
  2. Обобщение. Куда более редкий кейс, ведь у нас есть props. Явное лучше неявного. Если компонент позиционирован как всеядный, то логично это сделать явно. Ну ок, допустим нам это не подходит. Но у нас есть возможность накрыть любой компонент context.Provider-ом с другим значением (если уж приспичило делать через context). Зачем DI?

Всякий раз когда я вижу очередную 9999 статью про DI меня терзает вопрос — во имя чего это всё? Вот смотрю я в статью и не вижу какую боль это снимает. Зато вижу сколько приседаний новых появилось. Это как "а давайте внедрим в проект redux, сделаем глобальный store, несмотря на то что он нам как ежу футболка", или "а давайте на каждый компонент напишем по снапшот тесту, сделаем 100% покрытие кода, премию дадут". Вот как-то так это всё выглядит. В чём benefits?


Плюс буквально всегда речь идёт о классах и их конструкторах. Чем больше пишу фронты тем больше избегаю классов и вообще любых вещей с мутабельным состоянием. В текущем проекте на 70к строк 3-4 класса, из которых 2 это ErrorBoundaries и ещё 2 это legacy. Но ладно, топить за ФП тут не буду.


Можно, на примере, скажем, i18n, показать какие проблемы решит внедрение inversify в типичный проект, где нужна локализация?! Ну и пусть ещё слой api тоже будет зависимостью (хз зачем, но пусть). И вот у нас есть условная кнопка "like post". Нажатие на кнопку вызывает api-method который отправляет на backend запрос. Надпись "like post" должна быть локализована согласно выбранному языку. Итого 2 зависимости которые мы можем захотеть подменить.


Тесты — просто оборачиваем двумя <context.Provider value={fakeApi}/> и таким же для i18n. Либо вообще мокаем import { useTranslate } from ... и даже контекст не надо подменять.


Ну бог с ними тестами. В каком-то месте приложения мы решили подсунуть другую кнопку. Она теперь переведена на французский и запросы шлёт в /dev/null. Странно? Ну и ладно. У нас есть context.Provider-ы. Решают проблему? Да. А ещё можно при желании это вынести в props кнопки и задавать явно.


И теперь на проект приходит крутой архитектор из %компания-мечты% и внедряет DI. И сразу так хорошо жить стало. Вопрос — что он сделал и в чём стало лучше?

Про DI тема не ко мне, тут согласен, как и с моками с помощью контекстов — если действительно в проекте нужно тестирование реакт-компонентов (что бывает в одном случае на тысячу, т.к. только интеграционные и e2e тесты могут что-то гарантировать). А вот про классы и глобальные сторы — спорно, мне скорее интересно, чем хуковые портянки и локальные состояния лучше.


Про хуки мы уже разговаривали, там нужны очень конкретные и подходящие условия, чтобы снизить количество болей. Но чем глобальный стор насолил кроме отсутствия возможности сделать 2 разных под капотом, но одинаковых с точки зрения использования в компоненте подстора? Эта ситуация, опять же, решается вложенным контекстом, и если один-два места таких будут на большое приложение (а больше вряд ли понадобится) — то велика беда от такого подхода не будет. А вот необходимость иметь доступ ко всему состоянию приложения возникает часто, и локальные сторы во всем проигрывают глобальным. Также при разбиении кода на независимые модули с общим рутом в любом случае будут глобальные компоненты — модалки, нотификации, роутинг, та же локализация. Также придется внутри самих компонентов разруливать вычисление состояния исходя из локального и глобального — почему сразу не делать глобальным? Если на странице 5 форм — никто не заставляет их пихать в единый глобальный store.formData, разумеется тут будут store.forms.firstStep, store.forms.subscribeToNews и т.п.


Если же компонентов на странице много и они однотипные — то это будут store.tooltips[{ label, isOpen, ref }] с простой возможностью все подсчитать, закрыть при необходимости и удалить любой из любой части интерфейса. С локальными состояниями это просто невозможно без системы доступа к ним — а она обычно корява либо как this.componentRef.querySelector('[data-tooltip]')[0] либо как доступ к innerState через разнообразные хаки. Интерфейс на странице в любом случае имеет глобальную раскладку, глобальные стили, так почему его состояние должно дробиться на множество независимых и сложносинхронизируемых?


С глобальным многое становится более красивым, тот же онбординг будет состоять из одноуровневого и независимого вызова функций (редкий кейс, когда пригодятся генераторы, и можно по клику на кнопку вызывать следующий шаг)


function handleOnboardingStep* (store) { // лучше, конечно, не store а actions
  yield store.openTooltip(...)
  yield store.highlightArea(store.tooltips[0])
  yield store.closeTooltip(...)
  yield store.openModal(...)
  ...
}

Сериализация состояния и SSR тоже намного проще. Логирование действий и просмотр пользовательского пути для дебага. Computed селекторы, выдающие значение исходя из состояния приложения. Изолированность от библиотеки рендеринга. Вот никогда не упирался в невозможность или неудобство использования глобальных состояний, они и очищаются прекрасно, и ресетятся, а вот с локальными — сплошные неудобства.

Вы знаете — давайте лучше про глобальный store где-нибудь в другом месте поговорим :) Я его и люблю и ненавижу одновременно и мне тоже есть что сказать. Но всё таки этот топик про DI :) А то hate-topic-ов про redux/flux и без того половина хабра.

Без проблем, я просто вбросил то, что считаю "серьезной аргументацией", избавив других комментаторов от потенциальных споров по этой теме — пусть и правда лучше про DI, хотя кому он в общем нужен, со всеми недостатками...

и локальные сторы во всем проигрывают глобальным

Кроме разве что того, что код очень быстро превращается в глобальную портянку чего-то неподдерживаемого и очень вонючего.


Вот никогда не упирался в невозможность или неудобство использования глобальных состояний, они и очищаются прекрасно, и ресетятся

А локальные состояния в принципе не надо очищать и ресетить.


а вот с локальными — сплошные неудобства.

Например, какие? Писать меньше более простого кода для решения тех же задач при помощи глобального состояния? :)


Если на странице 5 форм — никто не заставляет их пихать в единый глобальный store.formData, разумеется тут будут store.forms.firstStep, store.forms.subscribeToNews и т.п.

Но зачем? Можно же просто сделать состояние этих форм локальным. Будет ровно все то же самое, что в случае с глобальным состоянием, но проще.


С локальными состояниями это просто невозможно без системы доступа к ним — а она обычно корява либо как this.componentRef.querySelector('[data-tooltip]')[0] либо как доступ к innerState через разнообразные хаки

Как это невозможно? Точено так же и получаете доступ, как в случае глобального состояния — но только без лишних церемоний.


Любая работа с данными в глобальном состоянии всегда эквивалентна работе с данными в локальном + нечто дополнительное.


yield store.openTooltip(...)
yield store.highlightArea(store.tooltips[0])
yield store.closeTooltip(...)
yield store.openModal(...)

Экшены — это события. Нельзя делать экшены вроде "openTooltip", т.к. открытие тултипа — не событие. Если вы делаете как выше, то вы теряете весь смысл глобального стора как такового — отделение логики работы с данными от представления.

"лишние церемонии", "глобальная портянка", "сложнее" — это явно от болей работы с редаксом, для mobx это все неактуально. "Точечный доступ" как раз увеличивает связность компонентов и добавляет лишние церемонии, это горизонтальное и parent-child общение между компонентами приводит к большой запутанности. Я описывал неудобства, повторять смысла не вижу.


Почему формы должны быть на глобальных конфигах — отдельная большая тема, основные преимущества — возможность легко получить доступ к данным формы из других частей приложения и конфигурировать ее, универсально обрабатывать валидации с бэка, возможность делать многошаговые формы, удобная сериализация для ssr.


Экшены — не события, а модификаторы стора с возможностью асинхронных вызовов. Слой событий обычно держится рядом с view:


handleClick = (...) => this.context.actions.openTooltip(...)
render = () => <button onClick={this.handleClick} />

Если бы я писал yield findReactElement(Tooltip)[0].handleClick(...) — тогда да, получается лез бы во внутреннее состояние и хендлеры событий — и это возможно только в случае, если этот компонент уже отрендерен, но скрыт стилями (эти хаки нужны только при подходе с локальным состоянием). В описанном мной подходе вызываются лишь экшены — модификаторы стора, которые никак не зависят от view и не знают о нем, а просто меняют данные, после чего store рассылает во view уведомления с изменениями и реакт ререндерит представление.


Если у вас есть более удобные способы работы функции handleOnboardingStep с локальными состояниями — буду рад почитать.

"Точечный доступ" как раз увеличивает связность компонентов и добавляет лишние церемонии, это горизонтальное и parent-child общение между компонентами приводит к большой запутанности

Но ведь все ровно наоборот. Ограничение доступа к данным — это вообще основной метод борьбы со сложностью.


Перенос данных в глобальный стор — этот то же самое, что вместо:


function f(x, y) {
    const z = x + y;
    return z;
}
console.log(f(1, 2));

писать:


let x, y, z;
function f() {
    z = x + y;
}
x = 1;
y = 2;
f();
console.log(z);

Но никто так в здравом уме не делает по очевидным причинам. И это все те же причины по которым очень, очень плохо совать данные в глобальный стейт.


Я описывал неудобства.

Где?


Почему формы должны быть на глобальных конфигах — отдельная большая тема

  1. Данные форм не должны быть глобальными
  2. Формы не должны быть на конфигах в принципе впрочем, я могу тут неверно интерпретировать, что вы подразумеваете под "на конфигах").

возможность легко получить доступ к данным формы из других частей приложения

Именно за тем данные форм и должны быть локальными, чтобы у вас не возникало искушения работать с ними в других частях приложения.


Экшены — не события, а модификаторы стора с возможностью асинхронных вызовов.

Это не так. Если вы интерпретируете экшены как модификаторы стора, то вам не нужны экшены как таковые.
Смысл использования экшенов вместо обычных ф-й, которые напрямую апдейтят данные в сторе — в том, чтобы апи модели не зависело от, собственно, самой модели данных.


В описанном мной подходе вызываются лишь экшены — модификаторы стора, которые никак не зависят от view и не знают о нем, а просто меняют данные

Т.е. вам не нужны экшены. Можно с тем же успехом использовать обычные ф-и, которые будут апдейтить модель напрямую, что приведет к ровно тому же результату, но без лишних ненужных прослоек.


(эти хаки нужны только при подходе с локальным состоянием)

Зачем?

Абстрактные примеры, боюсь, тут не к месту — все еще был бы рад увидеть, как вы в Реакте организуете доступ компонентов с локальными состояниями к данным друг друга, а также функцию handleOnboardingStep с похожим функционалом.


По поводу вашего примера — как раз так делают и это эффективней:


@makeObservable
class CountStore {
  x = 1;
  y = 2;
  get z() { return this.x + this.y }
}

// спроектировать экшен можно как угодно, я обычно делаю bind на
// глобальный стор и не передаю в параметрах вручную
function actionIncreaseX({ store }) {
  store.x = store.x + 1;
}

const FComp = observer(() => {
  const store = useStore(CountStore);

  return (<div onClick={() => actionIncreaseX({ store })}>{store.z}</div>)
})

Любой потребитель может подключиться к CountStore и использовать его данные — будь это autorun с сайд-эффектом записи изменившегося z в тайтл страницы или другой реактовый компонент, например — с инпутами, позволяющими явно указать x и y. С локальными состояниями все становится сложнее — сайд-эффекты придется описывать в самом компоненте, который хранит стейт, а передача другим компонентам x и y с функциями их установки — большой геморрой. Если это чайлды, то можно и через props, засорив этот механизм передачи данных, а если соседние элементы — то придется использовать некую шину в родительском компоненте, который уже будет передавать чайлдам эти данные и методы их изменения. Вряд ли кто-то выберет этот вариант, если в аргументации есть что-то кроме "очень, очень плохо совать данные в глобальный стейт".


Вообще диалог из разряда "сам дурак" получается, отвечу в той же манере. Данные форм должны быть глобальными. Формы должны быть на конфигах. Там. Затем. Экшены, про которые я говорил — обычные функции модификации стора aka методы, как в примере выше, а не то, что redux под ними подразумевает, мы явно не сходимся в терминологии, и явно вы говорите исходя из опыта на redux а не mobx — это разные миры, про недостатки редаксовых сторов и экшенов я сам могу много всего написать, но вроде на Хабре это уже не актуально.

Абстрактные примеры, боюсь, тут не к месту — все еще был бы рад увидеть, как вы в Реакте организуете доступ компонентов с локальными состояниями к данным друг друга

Не понял вопроса. Локальное состояние на то и локальное, что к нему доступа за пределами "локального пространства", в котором оно определено, нет. Иначе какое же оно локальное?


По поводу вашего примера — как раз так делают и это эффективней:

Серьезно? Ну покажите мне проект, в котором вместо локальных переменных в ф-ях используют глобальные. Такой код нигде ревью не пройдет.


Любой потребитель может подключиться к CountStore и использовать его данные

И это именно то, почему глобального состояния быть не должно. Потому что кто угодно откуда угодно может "подключиться и использовать данные". К данным должна быть возможность подключиться только у тех компонентов системы, которым эти данные необходимы. В данный момент в соответствии с текущими требованиями.


Вообще, откуда в голову приходит эта странная идея — подключать компонент к локальному стейту другого компонента? Вам же не приходит в голову "на всякий случай" вынести локальные переменные какой-то ф-и в глобальный стейт, потому что "вдруг в какой-то другой ф-и понадобится доступ к этой локальной переменной"? А почему не приходит? В чем разница?


С локальными состояниями все становится сложнее — сайд-эффекты придется описывать в самом компоненте, который хранит стейт

И это замечательно — мы можем строго гарантировать, что никто другой этот стейт не изменит. Именно поэтому любой стейт должен быть локален — если иное явно не нужно в силу требований к приложению.


. Вряд ли кто-то выберет этот вариант, если в аргументации есть что-то кроме "очень, очень плохо совать данные в глобальный стейт".

"очень плохо совать данные в глобальный стейт" — это старая истина, которая известна программистам уже много десятков лет (с тех пор как придумали понятие скоупа и модульную систему). Я вам выше привел пример того, как выглядит отказ от использования глобального стейта.
Любой перенос локального стейта в глобальный на порядок усложняет затраты на поддержку кода, т.к. вы не можете гарантировать потом корректность этого кода. Если ваш стейт глобален — то любые изменения в этом стейте могут совершенно случайным и непредсказуемым образом сломать что угодно в вашей программе. Если стейт локален — сломаться что-то может только в поддереве, при этом все зависимости данных явно обозначены.
Естественно, это предполагает определенной работы по грамотной декомпозиции — но в итоге вы получаете качественный код, который легко поддерживать. Только взглянув на данные (точнее — на их расположение) вы можете сразу сказать, кто и как их использует — что невозможно, когда данные лежат в глобальном стейте.
Если же все бездумно сгребать в глобальный стейт, как в мусорку — это и превращается в мусорку. И проект через несколько лет достигает фазы: "поддерживать невозможно, выкинуть — и переписать с нуля".


Экшены, про которые я говорил — обычные функции модификации стора aka методы, как в примере выше

Ну, так это не экшон, это и есть обычная функция.


а не то, что redux под ними подразумевает

Редакс всего лишь реализует один из многих стандартных подходов, который существовал еще до того, автор редакса родился. Так что нет никакого "редакс подразумевает" — это просто старые общеизвестные вещи. В мобх другая архитектура и, да, то что экшоны в мобх называются "экшоны" — это как раз из-за безграмотной попытки разделить с редаксом терминологию, а не наоборот.

Сразу проясню, под глобальными стейтами подразумевается не 1 огромный объект помойка, а конкретные разбитые состояния, типа UserState, NotificationsState и т.п.

Любой перенос локального стейта в глобальный на порядок усложняет затраты на поддержку кода, т.к. вы не можете гарантировать потом корректность этого кода.


О май гад, вот это конечно смешное утверждение. Если лично вы не можете гарантировать потом корректность этого кода или для вас это на порядок усложняет затраты на поддержку кода, то это сугубо ваши проблемы. НЕ имеющие отношения к другим людям. Например ни у меня, ни у команд в которых я работал никогда с этим не было проблем, только одни плюсы в виде супер быстрого и супер простого доступа к данным и методам того или иного глобального стейта.

И это именно то, почему глобального состояния быть не должно. Потому что кто угодно откуда угодно может «подключиться и использовать данные». К данным должна быть возможность подключиться только у тех компонентов системы, которым эти данные необходимы. В данный момент в соответствии с текущими требованиями.

И так
Потому что кто угодно откуда угодно может «подключиться и использовать данные»

Да, и это хорошо, если есть такая потребность. Зачем убивать эту возможность в виде идеального решения вынести стор в глобальную область видимости и супер удобного и супер быстрого доступа к его данным и методам? Вместо этого предлагаю убогие костыли из 2014 года.
Вы говорите "кто угодно откуда угодно" это вы что имеете ввиду? Что код пишете не вы, а машина сама его пишет, сама подключается куда ей вздумает и сама данными управляет да? Не вы это контролируете, а она да?

К данным должна быть возможность подключиться только у тех компонентов системы, которым эти данные необходимы.

Логично, ещё как должна быть, но вы отнимаете у них эту возможность говоря что глобальный стейт — говно. В реальности же к этому глобальному стейту и так подключаются только те компоненты, которым он нужен и никак иначе. Что за глупости, ну прям смешно читать.
Или у вас опять в вашем мире машины сами пишут код, подключатся куда им надо, а не вы всё это контролируете? Ведь если вы сами пишете код, то вы и любой человек легко увидит кто читает переменную X и кто вызывает метод Y.

Подсказка, приходится её часто давать, но увы почему-то люди о ней не знают, вы похоже не исключение.
WebStorm и VS Code умеют сто лет в обед — «Find References / Find Usages» где сразу вино в каких местах переменная читается, а в каких местах переменная изменяется.

Теперь зная эту секретную возможность вашей IDE, вы сможете легко и непринуждённо контролировать ваш код like a boss. И никакой глобальный стор вам не помешает, амнь.
Сразу проясню, под глобальными стейтами подразумевается не 1 огромный объект помойка, а конкретные разбитые состояния, типа UserState, NotificationsState и т.п.

Тут тогда сразу вопрос — в чем смысл выделения такого стейта в глобальный? Пусть локально и сидит, там где нужен.


Например ни у меня, ни у команд в которых я работал никогда с этим не было проблем, только одни плюсы в виде супер быстрого и супер простого доступа к данным и методам того или иного глобального стейта.

Вы здесь делите на ноль. Если можно супер быстро и супер просто получить доступ к данным — значит, он будет получен (аиначе какой смысл в этой быстрости и простости?), а если к данным есть доступ откуда угодно — то тогда вам и на порядок сложнее гарантировать корректность.
Сложность доступа к данным — это не баг, это фича, это вполне намеренное решение — скрывать данные так, чтобы "кто угодно" не мог получить к ним доступ.


Да, и это хорошо, если есть такая потребность.

Ну так никто и не говорит что глобальные стор использовать нельзя никогда и ни в коем случае. Когда потребность есть — используем, а когда нет (в 99% оставшихся случаев) — оставляем данные локальными.


Вы говорите "кто угодно откуда угодно" это вы что имеете ввиду? Что код пишете не вы, а машина сама его пишет, сама подключается куда ей вздумает и сама данными управляет да? Не вы это контролируете, а она да?

Код пишу не только я, а и множество других людей, при этом они могут сменяться со временем. И даже в том случае, когда я могу их проконтролировать (что, вообще говоря, далеко не всегда — я уж точно не могу проконтролировать тех, кто писал код до меня и тех,.кто будет писать после), то я как минимум, не хочу тратить на это время, если это не является необходимым. Грамотно выбранная архитектура позволяет мне это делать, она сама вместо меня следит за Васей, Петей и остальными. На мой взгляд — это очень удобно, т.к. позволяет мне сосредоточиться на своих делах, а не заниматься работой надсмотрщика.


Логично, ещё как должна быть, но вы отнимаете у них эту возможность говоря что глобальный стейт — говно.

Нет, не отнимаю. С какой стати? Я положил данные локально в компонент, значит, только этому компоненту они и нужны. Другим — нет. Зачем их класть в глобальный стейт?

Сразу проясню, под глобальными стейтами подразумевается не 1 огромный объект помойка, а конкретные разбитые состояния, типа UserState, NotificationsState и т.п.

Тут тогда сразу вопрос — в чем смысл выделения такого стейта в глобальный? Пусть локально и сидит, там где нужен.

Потому что доступ к ним нужно иметь из любой части приложения, проверить авторизован ли пользователь, как его зовут, есть ли у него права на доступ, вызвать метод который покажет нам какую-то нотификашку.

Если можно супер быстро и супер просто получить доступ к данным — значит, он будет получен (аиначе какой смысл в этой быстрости и простости?), а если к данным есть доступ откуда угодно — то тогда вам и на порядок сложнее гарантировать корректность.

Нет. Сложности не добавляется от слова совсем, а наоборот всё становиться только сильно проще и гарантировать корректность разумеется тоже гораздо проще, потому сам код и логика значительно проще.
Сложность доступа к данным — это не баг, это фича, это вполне намеренное решение — скрывать данные так, чтобы «кто угодно» не мог получить к ним доступ.

Нет, это баг. Причём критический и абсолютно ничем не обоснованный. Которой просто по приколу и на ровном месте усложняет и засоряет код, делая его грязным и не очевидным, плюс вставляет постоянно палки в колеса просто потому что вы думаете что так будет лучше. Но увы это глубокое заблуждение.

Код пишу не только я, а и множество других людей, при этом они могут сменяться со временем. И даже в том случае, когда я могу их проконтролировать (что, вообще говоря, далеко не всегда — я уж точно не могу проконтролировать тех, кто писал код до меня и тех,.кто будет писать после), то я как минимум, не хочу тратить на это время, если это не является необходимым. Грамотно выбранная архитектура позволяет мне это делать, она сама вместо меня следит за Васей, Петей и остальными. На мой взгляд — это очень удобно, т.к. позволяет мне сосредоточиться на своих делах, а не заниматься работой надсмотрщика.

Ещё раз продублирую
WebStorm и VS Code умеют сто лет в обед — «Find References / Find Usages» где сразу видно в каких местах переменная читается, а в каких местах переменная изменяется.

То есть, если вдруг у вас появляется баг или ситуация, где что-то не там вызывается / не там изменяется — вы элементарно можете отследить где меняется переменная или где вызывается тот или иной метод. Вы сразу же это увидите и пофиксите — профит. Плюсом вы сэкономили кучу времени и нервов на НЕ использовании громоздкой и никому не нужной архитектуры(кроме вас одного) с ультра сложным и геморройным доступом к данным и методам.

Теперь вопрос, в чем проблема тогда, если всё прекрасно прослеживается легким движением руки? И так же легко фиксится!

Даже если вы скрываете данные и ограничиваете к ним доступ, всё равно ведь доступ к ним можно получить, хоть и через жопу в вашей архитектуре и так же вызвать неправильно метод и так же не правильно изменить данные. Так в чем разница то? Что там в итоге можно читать/изменять данные что сям. Только в одном случае всё элементарно, в другом неоправданно усложнено.
Потому что доступ к ним нужно иметь из любой части приложения

Ну, так если нужен — выделяем. Однако, подавляющему большинству данных глобальный доступ — не нужен.


Нет. Сложности не добавляется от слова совсем, а наоборот всё становиться только сильно проще и гарантировать корректность разумеется тоже гораздо проще

Нет, не проще, т.к. вам надо проконтролировать больше мест, в которых эти данные обновляются.


Нет, это баг.

Нет, это фича. Я специально организовываю данные так, чтобы они не было доступны тем, кому не нужны.


Которой просто по приколу и на ровном месте усложняет и засоряет код, делая его грязным и не очевидным

Каким же образом? Наоборот — код становится проще и понятнее, т.к. все зависимости по данным становятся более очевидными.


Ещё раз продублирую

Не понял, каким образом вебсторм и вскод гарантируют, что Петя не будет писать говнокод?


То есть, если вдруг у вас появляется баг или ситуация, где что-то не там вызывается / не там изменяется — вы элементарно можете отследить где меняется переменная или где вызывается тот или иной метод

А я не хочу за этим следить, я хочу, чтобы никто в принципе эти данные не менял ни в каком месте кроме того, где я их определил.


Вы сразу же это увидите и пофиксите — профит

  1. Зачем на что-то смотреть и фиксить, если можно просто не допустить возникновения? Лучшее лечение — это профилактика.
  2. Пофикшу, собственно, как? Петя что-то наговнякал через глобальный стор, хотя так было делать не надо, и теперь вместо Пети я должен думать как ему надо было организовать данные, рефакторить за него код и все исправлять? Спасибо, но нет. Работу Пети пусть делает Петя.

Плюсом вы сэкономили кучу времени и нервов на НЕ использовании громоздкой и никому не нужной архитектуры(кроме вас одного) с ультра сложным и геморройным доступом к данным и методам.

Я не могу понять, в чем громоздкость и сложность архитектуры. Вот у меня есть компонент, вот в нем локальный стейт. Что тут сложного и громоздкого?


Даже если вы скрываете данные и ограничиваете к ним доступ, всё равно ведь доступ к ним можно получить, хоть и через жопу в вашей архитектуре и так же вызвать неправильно метод и так же не правильно изменить данные.

Можно, но проще написать код правильно. Поэтому ленивый Петя не будет писать говнокод — ведь говнокод писать тяжело! Он напишет хороший код — который написать было просто.


Сделать так, чтобы говнокод было писать сложнее, чем хороший код — в этом весь смысл существования архитектуры как таковой.

Ахахха, жесть. Я вообще не понимаю с какого перепуга вы думаете что глобальное состояние это значит что абсолютно всё состояние в приложении глобальное у такого понятия как локальный стейт компонента не существует, все глобальное. Ну что за бред то.

Вообще я че-то не понял, вы называете «архитектурой» локальный стейт компонента? И всё ваше прячу, ограничиваю и т.п. это всё лишь о том, что у компонентов есть локальный стейт? :D

Ну, так если нужен — выделяем.

Ну, так если нужен — выделяем. Вот именно, как только доступ нужен глобально, мы его выделяем глобально. Спасибо капитан.
Однако, подавляющему большинству данных глобальный доступ — не нужен.

И что с того? Ну не нужен, так они его не используют, а как нужен станет будут использовать. Вот и всё. В чем проблема то?

Дальше я пишу исходя из того, что думаю, что вы называя свою архитектуру архитектурой, вкладываете что-то другое в это понятие, а не только локальный стейт у компонентов. И у вас там какие-то обвязки, абстракции и т.д и т.п. и вот вы с помощью них как-то манипулируете состоянием приложения при этом как-то там ограничиваете доступы, ну короче вот в таком духе. Да и код можно не проверять, он в вашей архитектуре априори сразу правильный пишется любым человеком.

Нет, не проще, т.к. вам надо проконтролировать больше мест, в которых эти данные обновляются.

Нет, проще. Не надо ничего контролировать, всё и так абсолютно прозрачно и очевидно и не скрыто за завесой «архитектуры». (См. выше что я думаю у вас за архитектура)

Нет, это фича. Я специально организовываю данные так, чтобы они не было доступны тем, кому не нужны.

Поздравляю. Вы открыли существование локального стейта и глобального. В каждом случае применяется тот или иной.

Каким же образом? Наоборот — код становится проще и понятнее, т.к. все зависимости по данным становятся более очевидными.

Нет. Всё с точностью да наоборот. (См. выше что я думаю у вас за архитектура)

Не понял, каким образом вебсторм и вскод гарантируют, что Петя не будет писать говнокод?

Не понял как ваша «архитектура» будет гарантировать что Петя не будет писать говнокод? Ваши слова «ну он не сможет, потому что архитектура бла бла бла» вообще не канают. Потому что это не правда, говнокод можно написать абсолютно всегда, абсолютно везде и абсолютно при любых условиях. И сломать всё можно точно так же абсолютно везде и при любых условиях.

А я не хочу за этим следить, я хочу, чтобы никто в принципе эти данные не менял ни в каком месте кроме того, где я их определил.

Вы не следите — Петя делает всё что хочет, возьмет и удалит ваш говнокод, возьмет и поставит while (true) {...} и т.п. Ах да, что это я, у вас же «архитектура», даже если все это сделает то ничего не сломается и будет работать прежде, ведь вы за этим не следите, за этим следит ваша «архитектура».
Я не могу понять, в чем громоздкость и сложность архитектуры. Вот у меня есть компонент, вот в нем локальный стейт. Что тут сложного и громоздкого?

Это не архитектура. Это локальный стейт компонента. Есть такое понятия да, локальный и глобальный. Вот компоненты управление которых не должно быть из разных мест в приложении, у них стейт локальный, а те у кого должно — глобальный. Это же очевидно. Но вы почему-то думаете если кто-то сказал «глобальный стейт», то этой значит локальных состояний у компонентов нет и быть не должно, всё только глобальное и никак иначе :D

Так что в итоге вы подразумеваете под вашей «архитектурой» то? И почему всё же в ней нет места глобальным стейтам, разумеется которые глобальные не просто так, а потому что нужно иметь к ним доступ из разных мест. А то меня терзают всё таки смутные сомнения.
Я вообще не понимаю с какого перепуга вы думаете что глобальное состояние это значит что абсолютно всё состояние в приложении глобальное у такого понятия как локальный стейт компонента не существует, все глобальное

Это именно тот кейз, который мы обсуждали в данной ветке — "тащить все в глобальный стейт по дефолту".


Потому что это не правда, говнокод можно написать абсолютно всегда

Вопрос в сложности написания говнокода.


Это же очевидно. Но вы почему-то думаете если кто-то сказал «глобальный стейт», то этой значит локальных состояний у компонентов нет

Нет, я так не думаю. но некоторые люди предпочитают в принципе не использовать локальный стейт, а весь стейт делать глобальный. Именно недостатки этого подхода мы и обсуждали в данной ветке. А вы ворвались, не разобравшись :)

То есть вы обсираете не глобальные состояния как таковые, а подход где есть только глобальные состояния и нет локальных, и даже компонент с которым никто из вне не взаимодействует, обладающий каким-то состоянием и вот его состояние все равно выкинуто в глобальное, да?)

А вообще:

Абсолютно все состояния компонентов делать глобальными конечно смысла нету никакого, только по мере необходимости. Как только с ними нужно взаимодействовать из разных мест, тогда их состояние должны становиться глобальными именно для удобного и быстрого доступа к данным/методам. Все глобальные состояния должны хранится в специальной папке с глобальными состояниями, а локальные прямо рядом с компонентами, это вроде и так очевидно.
То есть вы обсираете не глобальные состояния как таковые, а подход где есть только глобальные состояния и нет локальных, и даже компонент с которым никто из вне не взаимодействует, обладающий каким-то состоянием и вот его состояние все равно выкинуто в глобальное, да?)

Мне казалось, что это совершенно ясно из контекста.


Все глобальные состояния должны хранится в специальной папке с глобальными состояниями, а локальные прямо рядом с компонентами, это вроде и так очевидно.

Ну вот DmitryKazakov8 — не очевидно.

Основной принцип: никто не должен иметь доступа ни к чему к чему не надо. Т.е.:


как вы в Реакте организуете доступ компонентов с локальными состояниями к данным друг друга

Никак. А если кто-то смог — на код-ревью бить линейкой по руками. И такой код revert-ить.


Любой потребитель может подключиться к CountStore и использовать его данные

Тут та же песня. Никак. Ограничивать доступ. Чтобы НЕЛЬЗЯ было кому попало обратиться к CountStore. Любые связи которые быть должны — должны быть заданы явным прослеживаемым способом. Никаких "я могу из любой части проекта залезть в любую часть проекта".


Соответственно всё сводится к:


  • это локальный стейт. Никому сюда нельзя
  • это, скажем, модульный стейт. Тогда он должен быть определён так высоко как нужно. И пробрасываться сверху-вниз всем потребителям. И тем что пишут в него, и тем что читают.

Вариантов много как это реализовать. Из очевидного props и context. Писанины получается, разумеется больше, чем при глобальном сторе. Но оно того стоит. Такой код выдерживает проверку временем. В таком коде сложно выстрельнуть себе в ногу. Такой код заставляет глубоко разбираться в бизнес-логике, в причинно-следственных связях и в итоге организовывать потоки данных так как надо, а не так как удобнее. Ибо второе со временем приводит к гниению и очень высокому техническому долгу. Тот самый случай когда, стоимость внесения изменений так высока, что пора переписывать проект.


Вот о чём я. low in coupling and high in cohesion мантра.


В тех проектах где у меня было много redux я придерживался тех же принципов. Я ломал все возможности залезть куда не надо. Изолировал всё от всего. Несмотря на принадлежность к общей глобальной переменной (что по сути и есть redux). У меня всё ещё оставалась возможность сериализации и десериализации, time-machine, простота дебага и пр… Но практика показала что хорошая архитектура обязательно ЧЁТКО разделяет что должно быть общим, а что нет. И никакого произвольного доступа к данным быть не должно.


Условно вот такой код в redux — это "код красный":


const withRedux = connect((state: TRootState) => state.whatever);

Ибо ничего нельзя брать из корня. Это illegal :)

У меня как раз обратный опыт, и большинство проектов с тьмой локальных сторов приводят к мешанине, высокой стоимости изменений и техдолгу, и приходилось все это переписывать заново. Единый расширяемый корень, одна точка доступа к данным — это полное отсутствие бойлерплейт-кода, изолированность и цельность state, независимость от view, стабильность, переиспользуемость, удобство создания computed селекторов, доступность из любой точки — и это не недостаток, а благо. Все точки использования легко находятся через findUsages.


Это никак не противоречит модульности — в примерах выше я показал, как он расширяется и очищается, и если модулю (странице) нужны 10 сторов, которые используются только на этой странице, то они легко добавляются и могут использоваться только на этой странице. То есть легко построить и "микрофронтендную" архитектуру из нескольких репозиториев со всегда единой схемой подключения store и возможностью использования более высокоуровневых данных и методов (модалки, нотификации, i18n).


Явные прослеживаемые связи — эта мантра идеально ложится на единый глобальный стор, так как 1 быстрый переход — и ты в модели, одним поиском находишь все места, в которых этот параметр использовался и мог изменяться, а не бегаешь по пропсам, чтобы найти родителя, который содержит в себе локальный стейт, из которого выстраивается локальный стейт данного компонента.


При этом вот этих "как сделать? Никак" становится крайне мало. Возможно, вы не работали с годными реализациями глобального стейта. То, о чем вы говорите — там есть, но добавляется возможность свободного доступа и независимого от view изменения. Слой данных по моей убежденности должен быть независим, как и слой сайд-эффектов, как и слой апи, как и слой стилей, как и слой модификаторов состояния. А все эти useState, useEffect с изменением стейта и сайд-эффектами, бесконечный props drilling, который приводит к ререндеру цепочки родителей, хотя только конечному компоненту нужны данные — это тот самый техдолг, а не его преодоление. Ну и SSR, видимо, у вас не используется.

Вы сейчас на полном серьёзе пытаетесь мне доказать, что граф произвольных зависимостей проще в разработке, нежели обычное древо. Зачем?

Честно? Это исходит из моих исследований реальности. В физическом мире много ограничений — ограниченные скоростью передатчики (свет, волна), законы преобразования приложенной силы, потери и искажения сигнала. Чтобы "коснуться" материальных деталей (планет, например) на большом расстоянии, придется проделать длинный props drilling, при этом актуальность сигнала уже пропадет и не будет отражать первоначальное намерение, потому что его источник уже мог перестать существовать.


При этом общее состояние мира является сбалансированным и стабильным, изменения в одной области в любом случае повлияют на все остальное, из чего я делаю вывод о "глобальности состояния", но несовершенстве его синхронизации. В программировании я преодолеваю эти несовершенства драфтово называемой "портальной архитектурой", когда есть единый целостный стейт, к которому может "портально" (react context + observer) подключиться любой видимый (view) или невидимый (side effect) потребитель, при этом изменения в хранилище моментально (синхронный event emitter) распространяются до тех потребителей, которым нужны конкретно эти данные (reaction subscribe). Так достигается целостность всей системы, максимальная скорость, независимость состояния и при этом его синхронизированность с отображением.


Почему это проще? Потому что не нужно получать стейты от неограниченного количества источников и синхронизировать их, как-то из props+local state+global state, у которых разные механизмы передачи информации и разные жизненные циклы. Почему проще именно в разработке? Потому что не нужно создавать цепочку передачи информации, которая приводит к ререндеру всех передатчиков и нестабильности финальных данных (проходя через props нескольких компонентов велик соблазн их модифицировать, что встречается повсеместно; из реальности — свет фильтруется, искажается его направление, рассеивается).


Хотя я не так давно практикую этот паттерн, пока не натыкался на те недостатки, которые вы предсказываете в виде потребления данных и их изменения теми, кому это не нужно — если не нужно, то компоненты и сайд-эффекты просто не подписываются на данный набор данных. Это не произвольные, а очень прямолинейные взаимосвязи — как ветки, листья и плоды присоединяются к единому стволу дерева, но каждый берет то, что нужно именно ему, и отдает то, что подходит всей системе (энергия из преобразованного листьями света).


А вот с локальными стейтами, хотя у вас и не так, так как тщательно следите в команде за изолированностью и иерархичностью, часто растет количество горизонтальных связей и дубляж, ограничивая возможности по выделению четких слоев и реальной изоляции компонентов, а не создания толстых взаимосвязанных структур, в которых перемешан и стейт, и сайд-эффекты, и синхронизация интерфейса.


Я думаю, что не только в программировании, но и в реальности за этим подходом будущее. Проводятся исследования со связанным фотонами, которые моментально реагируют на изменение характеристик друг друга вне зависимости от расстояния. Так, сейчас радиовышка вынуждена распространять сигнал по всем направлениям, что вызывает необходимость повышенного энергопотребления и ведет к ослаблению сигнала. Если сигнал нужен конкретному приемнику на большом расстоянии, то приходится вычислять его относительное расположение и передавать узконаправленный сигнал (props drilling, иерархичная связь между компонентами). Если бы приемник мог реагировать на сам факт подачи сигнала и синхронно воспроизводить его без искажений, то расположение, рассеивание, помехи ушли бы в прошлое. При наличии такой технологии единственный проблемный момент — авторизация, получение прав на синхронизацию с сигналом, та самая "изоляция", о которой вы так много говорите, но я не вижу в контексте фронтенда необходимости в подобном. Пусть каждый компонент-приемник потребляет ту информацию, которая ему нужна для функционирования. Если наткнусь на ограничения данного подхода, то первым скажу о них, но пока не сталкивался.

и ты в модели, одним поиском находишь все места, в которых этот параметр использовался и мог изменяться, а не бегаешь по пропсам

Я открою вам секрет, но без сваливания всех данных в глобальный стейт тоже не надо бегать по пропсам. Просто декомпозицию надо делать грамотно, а не абы как. Именно поэтому глобальный стейт и плох — он позволяет вам писать говнокод, ведь "просто взял и подключил компонент".

Прямые руки — самый ценный ресурс в программировании, все, что можем — создать набор эффективных практик для конкретной команды и проекта. Локальные тоже могут писать плохо, так как они же локальные и "всем все равно, как они реализованы".

Локальные тоже могут писать плохо, так как они же локальные и "всем все равно, как они реализованы".

Локальные сторы писать плохо — трудно и лениво. Необходимость далеко прокидывать пропсы вынуждает думать об организации данных.
В случае же глобального стора — полная свобода.

Вместо строковых констант я бы предложил сделать абстрактный класс Token, который мог бы в себе полиморфировать значение, т.е чем-то похожий на InjectionToken в Ангуляре

SOLID принципы ...1… 5
В рамках моего доклада нас интересует только последний принцип — принцип инверсии зависимостей.

Строго говоря, сабж — это прежде всего "принцип единственной ответственности". Модуль не берет на себя дополнительную задачу по резолву зависимостей. А пятый принцип — это скорее бестпрактис, как правильно делать. Но теоретически можно и без него.

Вместо строк можно использовать абстрактный класс и все хорошо типизируется, как это делают в angular.

Увидел много кода с классами, и так и не увидел кода компонента React, который использует данный подход. В статье есть только такое:


Теперь перейдем к компоненту, который будет получать из нашего контейнера необходимые данные в виде пропсов.

И все, дальше сбивчиво текстом объясняется, но я так и не понял, каким образом компонент объявляет свои зависимости, и каким образом это объявление читает HOC. Можно привести код хотя бы одного такого компонента? Про использование Inversify с обычным классами можно почитать в документации Inversify, я надеялся увидеть то, что заявлено в заголовке статьи.

Я так понимаю, что никакой проверки типа у второго параметра diInject нет? То есть, TypeScript не проверяет, что booksListModel: ListModel<IBook>; в объявлении Props и booksListModel: new Dependence(constants.booksListModelName), должны соответствовать друг другу, или хотя бы, что все зависимости переданы.

C хоками есть проблема такая проблема да… Особенно если в типе есть генерик. По большому счету хок говорит ts, что обёрнутому компоненту передавать эту зависимость из родительских компонентов не надо. У себя мы уже сделали хук, который проверяет все типы.

С учетом diInject у Вас получился Service Locator на самом деле, а не Constructor Injection. Как бы на уровне необернутого компонента вроде как есть Constructor Injection, но Вы все компоненты оборачиваете, и обернутые они уже просто берут что найдут в контейнере, и никто этот процесс не контролирует.

С учетом diInject у Вас получился Service Locator на самом деле, а не Constructor Injection.

Для функциональных компонент Constructor Injection невозможна по достаточно очевидным причинам


hint: у них нет конструктора :)

В функциональных компонентах — нет, по очевидным причинам.


hint: у них нет конструктора

Конструктор в обобщенном понимании — это тот интерфейс, через который осуществляется доступ к сущности. Именно так следует понимать слово "конструктор" в концепции "constructor injection", потому как смысл этой концепции — поместить зависимости там, где их невозможно проигнорировать, забыть, где о них подскажет компилятор. В ООП — это действительно конструктор класса (хотя не всегда, при использовании фабрик и других подобных паттернов объекты могут создаваться не через конструктор, и там тоже нужно подходить творчески к этому термину). В React роль конструктора выполняют props, так как не передав props мы не можем использовать компонент. Использование constructor injection в react должно подразумевать именно использование props.

В React роль конструктора выполняют props

Нет, не выполняют. Пропсы — это аргументы render-функции, а не аргументы конструктора. Доступа к конструктору функциональных компонент в реакте нет. Как нет и самого конструктора, впрочем (но это уже детали реализации — он мог бы быть но способ его вызвать или передать ему какие-то аргументы — по спецификации отсутствует).


Использование constructor injection в react должно подразумевать именно использование props.

Нет, для функциональных компонент constructor injection просто нет.


потому как смысл этой концепции — поместить зависимости там, где их невозможно проигнорировать, забыть, где о них подскажет компилятор

Нет, не так. Смысл этой концепции — в том, чтобы устанавливать зависимости во время создания объекта.

const Component = (props: Props) => {
  const ... = useMemo(() => {
    // constructor
  }, [/* empty */]);
}

Вот это, со скрипом, но всё же, сгодится в качестве конструктора

useMemo нежелательно, лучше useState/useRef:


Вы можете использовать useMemo как оптимизацию производительности, а не как семантическую гарантию. В будущем React может решить «забыть» некоторые ранее мемоизированные значения и пересчитать их при следующем рендере, например, чтобы освободить память для компонентов вне области видимости экрана.

Кстати да. Постоянно забываю об этом подводном камне.

Вообще это весело, кстати. Все юзают useMemo, типа для производительности, но в реальности в любой момент этот useMemo может полностью перестать работать и это даже не будет нарушением спецификации, кек.

Ну я не думаю, что они будут дропать кеш при каждом рендере. Так что в целом всё ок :) Просто не самый надёжный кеш, хех.

Ну я не думаю, что они будут дропать кеш при каждом рендере.

Да, это было бы странно офкос. Но суть в том, что они в принципе могут это сделать без нарушения имеющихся спек :)
Т.е. это, формально, будет изменение в минорном релизе, без нарушения обратной совместимости :)


Типа, ну и ладно, не кеширует наш useMemo теперь. А никто и не обещал, что будет, так то!

Хорошая статья. DI во фронтенд приложениях нужен еще как.
Где то год назад писал свою реализацию для TS s-di-injector только тут DI строиться через файл конфиг примерно как в Symphony.
Есть же bem-react
Зарегистрируйтесь на Хабре, чтобы оставить комментарий