Comments 6
Может я не прав и вы меня поправите, но в контексте реакта складывается ощущение, что некоторые недостатки никак не обыгрываются. Например, тот же props drilling. Вернее, обыгрываются но только для простых случаев.
Сферическая ситуация на основе кода в вашей статье.
Я компонент и я не хочу знать о существовании многих классов, имплементирующих некоторый интерфейс. Хочу знать только об одном интерфейсе логгера {log(): void}
, ну или в крайнем случае о базовом классе, поскольку типов в рантайме нет. Если один из родителей хочет установить для своих детей в качестве логгера некоторый логгер, наследующий базовый класс, но отличающийся от зарегистрированного, ему по всей видимости нужно создать свой контейнер зависимостей, зарегать класс, и обернуть детей провайдером.
Это при условии, что либа построит своё дерево контейнеров, и будет разрешать зависимости по всему дереву от текущей ветви к корню.
В той же ситуации есть ещё один нюанс. Если некоторый класс, который поднимается внутри компоненты, захочет взять этот экземпляр логгера, то просто напросто не сможет либо из-за реактовского ограничения на использование хуков, либо из-за необходимости сослаться на контейнер текущей ветви, как на глобальную переменную.
Итого, придётся как в старые добрые времена прокидывать всё добро аргументами. Ну разве что сейчас можно прокидывать не зависимости, а ближайший контейнер, если библиотека приспособлена.
По статье, мне не хватило хоть какого-нибудь описания жиненных циклов. Одного упоминания маловато.
Буду рад ошибиться, потому как реакт местами дико неудобный, и DI действительно теоеретически может помочь в больших проектах
В описанном кейсе действительно DI не решит проблему до конца. Но давай посмотрим с другой стороны:
можно ли зарегистрировать два логгера глобально и использовать нужный в нужном контексте (например, по
token
);почему возникла необходимость в отдельном подтипе логгера? Может быть, это можно решить внутри самого логгера — например, создать от него "копию" с другим поведением и передать её уже средствами React (через контекст/пропсы);
Когда мы говорим про использование DI в React-приложениях, важно понимать разницу в парадигмах. DI отлично подходит для бизнес-логики, инфраструктуры, управления состоянием вне UI — всего, что живёт "дольше", чем рендер компонента. React же предлагает свои механизмы для UI-слоя: props
, context
, state
, hooks
.
Я намеренно не реализовал иерархию контейнеров в @wroud/di
. Хотя такая фича часто востребована (см. Angular, InversifyJS), она влечёт за собой огромную сложность и тонны edge-кейсов. Зато без неё библиотека остаётся предсказуемой и простой в использовании, что особенно важно для frontend-проектов.
Основная сложность, как мне кажется, не в том, что DI не работает, а в том, что React не даёт чёткой модели для архитектуры приложения. Он предоставляет мощный рендеринг-движок, но не говорит, как разделять бизнес-логику, работу с API, глобальное состояние и прочее. Поэтому и получается, что у каждого проекта свой подход.
Наш опыт в CloudBeaver подтверждает, что DI в больших frontend-проектах даёт ощутимую пользу. Мы используем InversifyJS
(на тот момент @wroud/di
ещё не было), и да, поначалу многим было непросто. Но со временем стало понятно: DI помогает структурировать код и заставляет отделять бизнес-логику от UI, что делает приложение гораздо легче для поддержки.
Что касается жизненного цикла: он очень простой. Контейнер обычно создаётся один раз на старте приложения и передаётся в React через Context
. Есть три типа сервисов:
singleton
— один экземпляр на всё приложение;transient
— новый экземпляр при каждом запросе;scoped
— для серверных приложений, где нужен отдельный scope на каждый запрос. В UI этот режим почти не используется.
Надеюсь, это немного прояснит общую картину. DI — не серебряная пуля, но в определённых слоях приложения он может сильно упростить архитектуру.
Я компонент и я не хочу знать о существовании многих классов, имплементирующих некоторый интерфейс. Хочу знать только об одном интерфейсе логгера
{log(): void}
, ну или в крайнем случае о базовом классе, поскольку типов в рантайме нет. Если один из родителей хочет установить для своих детей в качестве логгера некоторый логгер, наследующий базовый класс, но отличающийся от зарегистрированного, ему по всей видимости нужно создать свой контейнер зависимостей, зарегать класс, и обернуть детей провайдером.
Вы не поняли идею контейнера. Контейнер - это единственная точка входа для внедряемых зависимостей. Он по определению один на все приложение. Если у вас приложение использует в разных местах разные реализации логеров - это означает, что внедряемых логер является конфигурируемым. И тогда родитель устанавливает конфигурацию логера для своих потомков, но экземпляр логера они получают из DI, а вот конфигурацию из контекста.
По сути, тоже самое выше ответили.
Использовали react + mobx + react-ioc. Очень понравилось такая связка. По сути react-ioc на wroud можно заменить, вроде не сильно отличаются
Пока что единственным инструментом который "продал" мне инъекцию зависимостей в жс стал effect. Почти все подобные инструменты пытались навязать мне запихивание любой сущности в класс и обмазывание декораторами, никогда не понимал почему кто-то считает что это удобно. Не говоря уже о том что никакой статической гарантии того что ты правильно построил зависимости не будет скорее всего, за этим сам следи. Эффект в сравнении со всем этим кажется таким простым и понятным.
Dependency Injection в JavaScript: зачем он вам нужен