Старые добрые принципы SOLID могут быть полезны в любой области разработки. Особенно важным я считаю принцип инверсии зависимостей . В React существуют механизмы, позволяющие реализовать этот принцип, и в этом очень помогает Typescript.
Предположим, что нам досталась задача разработать компонент List , отображающий список из нескольких ListItem .
Некоторое время спустя в другом месте понадобился похожий компонент, но без иконок и с бейджами.
Чтобы не дублировать код, мы реализовали механизм кастомизации List с помощью пропсов, позволяющих включать/выключать определённые параметры элементов списка.
API компонента получается недостаточно гибким. Например, если в какой-то момент нам понадобится скрыть иконку у отдельно взятых ListItem , мы не сможем этого сделать, поскольку пропса у List позволяет сделать это только для всех элементов сразу.
При текущем подходе реализация List напрямую зависит от конкретной реализации ListItem .
Следующей задачей стала реализация поиска в этом списке.
Для этого создадим на основе уже имеющихся компонентов новый — SearchableList .
Компонент SearchableList напрямую зависит от компонентов List и ListItem , поэтому мы снова сталкиваемся с проблемами, которые решали ранее.
Дело в том, что текущая реализация принципа инверсии зависимостей не раскрывает его полностью. List принимает на вход уже готовые элементы, хотя мог бы принимать функцию, создающую их (то есть компонент). Для этого мы можем описать интерфейс, которому должны соответствовать компоненты, реализующие элементы списка.
В таком случае компонент списка будет принимать на вход данные для списка.
Компонент SearchableList же расширяет поведение компонента List , сохраняя его API.
Теперь при использовании компонента List мы можем передать ему любую реализацию элемента списка, соответствующую описанному интерфейсу RenderListItem .
Таким образом все наши компоненты не имеют прямых связей между собой, а зависят от одного общего интерфейса.
Помимо гибкости разработки этот подход также упрощает тестирование за счёт того, что мы можем замокать реализацию элементов списка и протестировать сам список изолированно.
Мы разобрались с тем, как кастомизировать компоненты, но с помощью этого подхода мы можем переопределять что угодно, в том числе и логику. Логика в React реализуется с помощью хуков, которые также можно передавать через props.
Для этого опишем интерфейс, описывающий логику:
И сделаем компонент зависимым от этого интерфейса.
Теперь компонент SearchableList не имеет своей реализации поиска и мы можем определить её при использовании компонента:
Закономерным развитием инверсии зависимостей является внедрение зависимостей . Это механизм, позволяющий централизовать передачу зависимостей компонентам.
В React для внедрения зависимостей используется контекст . Можно перенести все компоненты, которые мы хотим кастомизировать, в контекст, и в случае необходимости переопределять их из одного места.
Например, создадим контекст для компонента ListItem :
Далее создадим хук для получения компонентов из контекста:
Наконец создадим провайдер, который позволит нам переопределить некоторые компоненты:
Для получения компонентов из контекста просто используем созданный хук:
В результате мы получаем возможность переопределять дочерние компоненты с помощью провайдера, причём каскадно, как в СSS:
Зависимости у нас остаются инвертированными, но теперь компоненты зависят не от интерфейсов, а от контекста.
Принцип инверсии зависимостей достаточно прост и его реализация не потребует больших трудозатрат, но существенно снизит связанность кодовой базы, что положительно скажется на её гибкости и снизит количество кода, которое понадобится изменить, чтобы внедрить новую функциональность.