Как и любой React-разработчик, вы наверняка задавались одним из следующих вопросов:

  • Как создать переиспользуемый компонент, подходящий для разных случаев?
  • Как создать компонент с простым API, упрощающим его использование?
  • Как создать расширяемый компонент в плане UI и функциональности?  

Популярность этих вопросов привела к разработке сообществом React продвинутых шаблонов.

В текущей статье я представлю обзор пяти таких шаблонов. А для простоты их сопоставления буду следовать единой структуре для каждого:

Сначала будет идти небольшое введение, сопровождаемое реальным примером кода (на основе одного и того же простого компонента Counter).

Весь исходный код доступен в репозитории : https://github.com/alex83130/advanced-react-patterns.

Я также буду перечислять список плюсов и минусов, а затем определять два фактора в разделе “Критерии”:

  • Инверсия управления: уровень гибкости и контроля, предоставляемый пользователям вашего компонента.
  • Сложность реализации: сложность реализации компонента как для вас, так и для ваших пользователей.

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

Будем рассматривать ситуацию, в которой React-разработчик создает Component для других разработчиков. Следовательно, действующее лицо “пользователи” будет относиться непосредственно к этим разработчикам, а не конечному потребителю вашего сайта или приложения.

1. Составные компоненты

Этот шаблон позволяет создавать выразительные и декларативные компоненты без излишнего пробрасывания (prop drilling (англ.)). Применение этого шаблона стоит рассматривать, когда требуется получить широко настраиваемый компонент с более четким разделением ответственности и понятным API.

Пример

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/compound-component

import React from "react";
import { Counter } from "./Counter";

function Usage() {
const handleChangeCounter = (count) => {
console.log("count", count);
};

return (
<Counter onChange={handleChangeCounter}>
<Counter.Decrement icon="minus" />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon="plus" />
</Counter>
);
}

export { Usage };

Плюсы

  • Уменьшение сложности API: вместо втискивания всех пропсов в один гигантский родительский компонент и их пробрасывания вниз к дочерним компонентам UI здесь каждый пропс прикрепляется к SubComponent, что оказывается более эффективным и удобным решением.
  • Гибкая структура разметки: компонент получает повышенную гибкость, позволяющую создавать на базе одного компонента различные случаи. К примеру, пользователь может изменить порядок нескольких SubComponent или определить, какой из них должен отображаться.
  • Разделение ответственности: большая часть логики содержится в основном компоненте Counter, а React.context используется для распределения states и handlers по потомкам. В итоге получается отчетливое разделение ответственности.

Минусы

  • Слишком высокая гибкость: наличие гибкости повышает вероятность возникновения неожиданного поведения (добавление ненужного потомка Component, нарушение порядка потомков Component, забывание включить необходимого потомка).

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

  • Утяжеление JSX: применение этого шаблона увеличит количество строк JSX, особенно если вы используете линтер вроде ESLint либо инструмент форматирования кода вроде Prettier. В масштабах одного компонента это может выглядеть несущественным, но определенно окажется важным при рассмотрении более обширной картины.

Критерии 

  • Инверсия управления: 1/4
  • Сложность реализации: 1/4

Публичные библиотеки

2. Управляющие пропсы

Этот шаблон преобразует компонент в управляемый компонент. Внешнее состояние принимается в качестве “единого источника истины”, что позволяет пользователю внедрять собственную логику, изменяющую предустановленное поведение компонента.

Пример

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/control-props

import React, { useState } from "react";
import { Counter } from "./Counter";

function Usage() {
const [count, setCount] = useState(0);

const handleChangeCounter = (newCount) => {
setCount(newCount);
};
return (
<Counter value={count} onChange={handleChangeCounter}>
<Counter.Decrement icon={"minus"} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon={"plus"} />
</Counter>
);
}

export { Usage };

Плюсы

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

Минусы

  • Сложность реализации: ранее одной интеграции в одном месте (JSX) было достаточно, чтобы компонент заработал. Теперь же он будет разбросан по трем разным точкам (JSX / useState / handleChange).

Критерии

  • Инверсия управления: 2/4
  • Сложность реализации: 1/4

Публичные библиотеки

3. Пользовательский хук

Немного углубимся в “инверсию управления”: теперь основная логика передается в пользовательский хук. Этот хук доступен для другого пользователя и выражает внутреннюю логику (States, Handlers), давая ему расширенный контроль над компонентом.

Пример

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/custom-hooks

import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

function Usage() {
const { count, handleIncrement, handleDecrement } = useCounter(0);
const MAX_COUNT = 10;

const handleClickIncrement = () => {
//Разместите свою логику здесь
if (count < MAX_COUNT) {
handleIncrement();
}
};

return (
<>
<Counter value={count}>
<Counter.Decrement
icon={"minus"}
onClick={handleDecrement}
disabled={count === 0}
/>
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment
icon={"plus"}
onClick={handleClickIncrement}
disabled={count === MAX_COUNT}
/>
</Counter>
<button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}

export { Usage };

Плюсы

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

Минусы

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

Критерий

  • Инверсия управления: 2/4
  • Сложность реализации: 2/4

Публичные библиотеки

4. Геттеры пропсов 

Шаблон “Пользовательский хук” дает повышенный контроль, но усложняет интеграцию компонентов, так как пользователю приходится иметь дело со множеством нативных пропсов хука и повторно прописывать логику на своей стороне. Шаблон “Геттеры пропсов” скрывает эту сложность. getter  —  это функция, которая возвращает много пропсов. Название функции говорит само за себя, позволяя пользователю естественным образом связывать ее с правильным JSX-элементом.

Пример

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/props-getters

import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;

function Usage() {
const {
count,
getCounterProps,
getIncrementProps,
getDecrementProps
} = useCounter({
initial: 0,
max: MAX_COUNT
});

const handleBtn1Clicked = () => {
console.log("btn 1 clicked");
};

return (
<>
<Counter {...getCounterProps()}>
<Counter.Decrement icon={"minus"} {...getDecrementProps()} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} {...getIncrementProps()} />
</Counter>
<button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
Custom increment btn 1
</button>
<button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
Custom increment btn 2
</button>
</>
);
}

export { Usage };

Плюсы:

  • Простота использования: предоставляет простой способ интеграции компонента. Вся сложность скрыта, и от пользователя требуется лишь связать правильного геттера с правильным JSX-элементом.
  • Гибкость: у пользователя по-прежнему сохраняется возможность перегрузки содержащихся в геттерах пропсов для адаптирования компонента к отдельным случаям.

Минусы

  • Недостаток прозрачности: привносимая геттерами абстракция упрощает интеграцию компонента, но также делает его более скрытым и “магическим”. Чтобы корректно переопределить компонент пользователю нужно знать список пропсов, выражаемых геттерами, а также понимать, какое влияние на внутреннюю логику окажет их изменение.

Критерии

  • Инверсия управления: 3/4
  • Сложность интеграции: 3/4

Публичные библиотеки

5. Редьюсер состояния

Самый продвинутый шаблон в плане инверсии управления. Он предоставляет пользователю более удобный способ изменения внутреннего функционирования компонента. Его код аналогичен шаблону “Пользовательский хук”, но здесь пользователь также определяет reducer, который передается в хук. Этот reducer будет перегружать любое внутреннее состояние компонента. 

Пример

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/state-reducer

import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;
function Usage() {
const reducer = (state, action) => {
switch (action.type) {
case "decrement":
return {
count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1)
};
default:
return useCounter.reducer(state, action);
}
};

const { count, handleDecrement, handleIncrement } = useCounter(
{ initial: 0, max: 10 },
reducer
);

return (
<>
<Counter value={count}>
<Counter.Decrement icon={"minus"} onClick={handleDecrement} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} onClick={handleIncrement} />
</Counter>
<button onClick={handleIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}

export { Usage };

В этом примере мы сопоставили шаблон “Редьюсер состояния” с “Пользовательским хуком”, но его также можно использовать с шаблоном “Составные компоненты” и передавать reducer напрямую в основной компонент Counter.

Плюсы 

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

Минусы:

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

Критерии

  • Инверсия управления: 4/4
  • Сложность интеграции: 4/4

Публичные библиотеки

Заключение

Рассмотрев 5 продвинутых шаблонов React, мы узнали различные способы эффективного использования принципа “инверсии управления”. Все эти шаблоны дают вам широкие возможности для создания гибких и адаптируемых компонентов.

Однако здесь стоит учесть, что более обширные возможности требуют повышенной ответственности. Чем больше вы передаете контроль пользователю, тем больше ваш компонент уходит из разряда “plug and play”. Именно на вас, как на разработчике, лежит бремя выбора корректного шаблона, соответствующего конкретным нуждам.

В качестве простой шпаргалки я предоставлю вам следующую схему, которая классифицирует шаблоны по двум факторам: “Сложность интеграции” и “Инверсия управления”:

Статья основана на удивительной работе Кента К. Доддса. В его блоге (англ.) вы можете более углубленно познакомиться с каждым из рассмотренных шаблонов. 

Спасибо за чтение!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Alexis Regnaud: 5 Advanced React Patterns

Предыдущая статья5 инструментов для специалистов по обработке данных
Следующая статья5 алгоритмов, которые изменили мир