Пишем API для React компонентов, часть 5: просто используйте композицию

Автор оригинала: Sid
  • Перевод
  • Tutorial
Пишем API для React компонентов, часть 1: не создавайте конфликтующие пропсы

Пишем API для React компонентов, часть 2: давайте названия поведению, а не способам взаимодействия

Пишем API для React компонентов, часть 3: порядок пропсов важен

Пишем API для React компонентов, часть 4: опасайтесь Апропакалипсиса!

Пишем API для React компонентов, часть 5: просто используйте композицию

Пишем API для React компонентов, часть 6: создаем связь между компонентами

У нас есть компонент значка:


badge-1


<Badge count={12} />

Вы видели их в различных приложениях, они показывают количество объектов в виде числа.


github-1


В cosmos Badge (значок) имеет несколько цветов для каждого конкретного контекста (информация, опасность и т.д.)


badge-2


<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.


github-2


У него то же есть несколько цветов для каждого контекста:


label-2


<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 (об их пропсах)


together 2


<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.


together 2


<Badge content="12" appearance="information" />
<Label content="private" appearance="information" />

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


Но подождите, в React-е уже есть многоцелевой content проп, он называется children — дочерний.


Не переизобретайте props.children.

Если вы определили пропсы, которые принимают произвольные данные, не основанные на структуре данных, вероятно, лучше использовать composition (композицию) — Brent Jackson

Вот совет этой статьи — При выборе между композицией и пропсами, выбирайте композицию.


Давайте проведем рефакторинг этого API при помощи children — дочерних элементов:


together 2


<Badge appearance="information">12      </Badge>
<Label appearance="information">Private </Label>

Выглядит отлично.


Бонус: когда вы вместо пропа text используете children, разработчик использующий этот компонент получает больше гибкости без необходимости изменять компонент.


К примеру, вот сообщение предупреждение, в нем, я хочу добавить иконку перед текстом.


alert


Используя 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 друзья, нужна небольшая помощь. Я продолжаю сталкиваться с этим шаблоном, где определенные компоненты (особенно списки) могут быть разделены на более мелкие компоненты или управляться путем передачи объекта. Какой из вариантов лучше?

code


Выглядит знакомо?


Прежде всего, давайте не будем использовать проп text и вместо этого будем использовать children.


// вместо этого:
<Breadcrumb text="Home" href="/child" />

// напишем это:
<Breadcrumb href="/child">Home</Breadcrumb>

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


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


  1. Вам не нужно думать о том как называется проп — text? label? Это просто children.
  2. Вы можете добавить свое className или target к нему, если нужно. Для второго варианта вам нужно убедиться, что он поддерживает эти свойства или просто передает их базовому элементу.
  3. Это позволяет обернуть дочерний элемент в контекст или в компонент более высокого уровня.

Исключение из правила:


Что, если Брэд хочет запретить разработчику выполнять какие-либо настройки, о которых я упоминал выше? Тогда давать разработчику больше гибкости, в его случае, будет ошибкой!


Вот мой ответ Брэду.


Больше примеров


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


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


// #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.

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    0
    А потом появляется лейбл с заливкой и бейдж без заливки. Итого это один и тот же компонент с разным скруглением. Но зато есть 2 разных компонента, делающих одно и то же.
      +1
      Дублирование лучше неправильной абстракции — Sandi Metz
      –3

      Ёмаё да сколькож можно то. Ну давайте уже кто нибудь запилит статью как сделать div на реакте?

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

        Непрофессиональный цикл статей, на мой взгляд. Даже в простейших примерах явные ошибки. То setState как будто синхронный, то текстовые пропы от руки каждый раз пишутся вместо констант, то вот label здесь без htmlFor. Если кто-то все же читает, лучше используйте такой паттерн:


        <Input.Text
          label="Name"
          storePath="forms.registration.email"
        />
        
        // и внутри компонента
        
        const id = generateId(props.storePath);
        
        return (
          <div>
            <label htmlFor={id}>{props.label}</label>
            <input type="text" id={id} value={getValueFromStore(props.storePath)}/>
          </div>
        )
        

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


        В примере storePath строкой задано, что при рефакторинге может вылезти в баг, поэтому тоже лучше генерировать storePath={pathToString(forms.registration.email)}

          0
          Хотя если проект на MobX то лучше storePath все же строкой, чтобы компонент формы не подписывался на обновление значения конкретных полей.
            0

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

              0
              Напишете пример?
                0

                Начать можно с этого:


                const expr_symbol = Symbol("Expr")
                type Lens<R, T> = {
                    get(root: R) : T,
                    set(root: R, value: T): void,
                }
                
                type Expr<R, T> = (T extends object ? {
                    readonly [K in keyof T]: Expr<R, T[K]>
                } : {}) & {
                    [expr_symbol]: Lens<R, T>
                }
                
                function expr<T>(root: T) : Expr<T, T> {
                    const handler : ProxyHandler<PropertyKey[]> = {
                        get(target, p, receiver) {
                            if (p === expr_symbol) return createLens(target);
                            return new Proxy(target.concat([p]), handler);
                        },
                    }
                
                    return <any>new Proxy([], handler);
                
                    function createLens(path: PropertyKey[]) : Lens<any, any> {
                        throw "TODO";
                    }
                }
                
                function lens<R, T>(expr: Expr<R, T>) :  Lens<R, T> {
                    return expr[expr_symbol];
                }
                
                // ...
                
                lens(expr(forms).registration.email);
                  0
                  Спасибо, вы правы, подход createProxy(store).registration.email.toString() / toString(createProxy(store).registration.email) может облегчить рефакторинг, если совпадут факторы: в проекте используется типизация, она сделана корректно, IDE ее понимает и корректно меняет параметры при рефакторинге.

                  В остальных случаях строка подходит отлично — findAndReplace 'store.registration.email' справится с той же скоростью и надежностью, пожалуй я перебдел в данном случае.

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

        Самое читаемое