Пишем API для React компонентов, часть 1: не создавайте конфликтующие пропсы
Пишем API для React компонентов, часть 2: давайте названия поведению, а не способам взаимодействия
Пишем API для React компонентов, часть 3: порядок пропсов важен
Пишем API для React компонентов, часть 4: опасайтесь Апропакалипсиса!
Пишем API для React компонентов, часть 5: просто используйте композицию
У нас есть компонент значка:
<Badge count={12} />
Вы видели их в различных приложениях, они показывают количество объектов в виде числа.
В cosmos Badge
(значок) имеет несколько цветов для каждого конкретного контекста (информация, опасность и т.д.)
<Badge count={12} appearance="information" />
<Badge count={12} appearance="success" />
<Badge count={12} appearance="default" />
<Badge count={12} appearance="warning" />
<Badge count={12} appearance="danger" />
У этого пользовательского интерфейса есть еще один похожий компонент — Label
.
У него то же есть несколько цветов для каждого контекста:
<Label text="private" appearance="information" />
<Label text="private" appearance="success" />
<Label text="private" appearance="default" />
<Label text="private" appearance="warning" />
<Label text="private" appearance="danger" />
Посмотрите на эти два компонента и скажите одну хорошую и одну плохую вещь об их API (об их пропсах)
<Badge count={12} appearance="information" />
<Label text="private" appearance="information" />
Что хорошо
У обоих компонентов есть одинаковый проп для внешнего вида: appearance
, это здорово. Мало того, у них одинаковые варианты для этого пропа! Если вы знаете как использовать appearance
в Badge
, то вы уже знаете как использовать appearance
в Label
Стремитесь к последовательным пропсам между компонентами
Совет № 2 из Пишем API для React компонентов, часть 2: давайте названия поведению, а не способам взаимодействия
Что плохо
То, как они принимают свои значения, отличается. У них обоих свой вариант.
Подсчет — count
, имеет смысл в рамках компонента Badge
, но с учетом всех остальных ваших компонентов это дополнительный API о котором придется помнить вашей команде и пользователям (разработчикам).
Давайте улучшим этот API
Чтобы бы быть последовательным, я назову этот проп content
, это наиболее общее название которое я смог придумать, — более общее чем просто label, text или value.
<Badge content="12" appearance="information" />
<Label content="private" appearance="information" />
Мы потеряли некоторую детализацию, но получили большую последовательность. Мы все еще можем установить тип значения с помощью prop-types, так что думаю, что это хороший компромисс.
Но подождите, в React-е уже есть многоцелевой content
проп, он называется children
— дочерний.
Не переизобретайте
props.children.
Если вы определили пропсы, которые принимают произвольные данные, не основанные на структуре данных, вероятно, лучше использовать composition (композицию) — Brent Jackson
Вот совет этой статьи — При выборе между композицией и пропсами, выбирайте композицию.
Давайте проведем рефакторинг этого API при помощи children
— дочерних элементов:
<Badge appearance="information">12 </Badge>
<Label appearance="information">Private </Label>
Выглядит отлично.
Бонус: когда вы вместо пропа text
используете children
, разработчик использующий этот компонент получает больше гибкости без необходимости изменять компонент.
К примеру, вот сообщение предупреждение
, в нем, я хочу добавить иконку перед текстом.
Используя children
я могу добавить иконку в это сообщение предупреждение
, не возвращаясь к этому компоненту и не меняя его.
// Плохо - приходиться добавлять поддержку иконок
<Alert type="warning" icon="warning" text="This is an important message!" />
// Хорошо
<Alert type="warning">
<Icon name="warning" /> This is an important message!
</Alert>
По совпадению, когда я писал этот текст, я увидел твит Брэда Фроста:
Эй, React друзья, нужна небольшая помощь. Я продолжаю сталкиваться с этим шаблоном, где определенные компоненты (особенно списки) могут быть разделены на более мелкие компоненты или управляться путем передачи объекта. Какой из вариантов лучше?
Выглядит знакомо?
Прежде всего, давайте не будем использовать проп text
и вместо этого будем использовать children
.
// вместо этого:
<Breadcrumb text="Home" href="/child" />
// напишем это:
<Breadcrumb href="/child">Home</Breadcrumb>
Теперь, когда мы разобрались с этим, давайте поговорим об этих двух вариантах API.
Как не сложно догадаться, мне нравится первый.
- Вам не нужно думать о том как называется проп —
text
?label
? Это простоchildren
. - Вы можете добавить свое
className
илиtarget
к нему, если нужно. Для второго варианта вам нужно убедиться, что он поддерживает эти свойства или просто передает их базовому элементу. - Это позволяет обернуть дочерний элемент в контекст или в компонент более высокого уровня.
Исключение из правила:
Что, если Брэд хочет запретить разработчику выполнять какие-либо настройки, о которых я упоминал выше? Тогда давать разработчику больше гибкости, в его случае, будет ошибкой!
Вот мой ответ Брэду.
Больше примеров
Вот еще несколько примеров того, как этот совет может улучшить ваш код, последний мой любимый.
Формы — отличный пример использования, мы хотим управлять макетом формы, отображать ошибки и т.д. Но в то же время мы не хотим лишаться возможностей для расширения.
// #1 Плохо
<FormTextInput
type="text"
label="Name"
id="name-input"
/>
// к чему относится этот id,
// к label или к input?
// #2 Хорошо
<FormField>
<Label>Field label</Label>
<TextInput id="name-input" type="text" placeholder="What's your name?" />
</FormField>
// #3 то же хорошо
<FormField label="Field label">
<TextInput id="name-input" type="text" placeholder="What's your name?" />
</FormField>
Последний пример особенно интересный.
Иногда вам нужен компонент который будет использоваться в очень разных ситуациях. Не просто сделать компонент который будет гибок, и при этом все еще иметь простой API.
Вот где на помощь приходит инверсия управления — пусть пользователь компонента сам решает что рендерить. В мире React-а этот паттерн называется render prop pattern (паттерн рендер-пропсов).
Компонент с рендер-пропом берёт функцию, которая возвращает React-элемент, и вызывает её вместо реализации собственного рендера.
из документации React Рендер-пропсы
Одним из наиболее популярных примеров рендер-пропсов является официальный Context API.
В следующем примере компонент App
контролирует данные, но не контролирует их рендеринг, он передает этот контроль компоненту Counter
(счетчик).
// создаем новый контекст
const MyContext = React.createContext()
// значение передается вниз
// через контекст провайдер
function App() {
return (
<MyContext.Provider value="5">
<Counter />
</MyContext.Provider>
)
}
// и потребляется через
// потребителя контекста
function Counter() {
return (
<MyContext.Consumer>
{value => (
<div className="counter">the count is: {value}</div>
)}
</MyContext.Consumer>
)
}
Заметили что-нибудь интересное в этом Consumer
API?
Вместо того чтобы создавать новый API, он использует children
, чтобы принять функцию которая скажет ему как рендерить!
// Плохо
<Consumer render={value => (
<div className="counter">the count is: {value}</div>
)} />
// Хорошо
<Consumer>
{value => (
<div className="counter">the count is: {value}</div>
)}
</Consumer>
Вернитесь к своему коду и найдите компонент который принимает какие либо пропсы, когда он, при этом, может легко использовать children
.
Автор: IvanGanev