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

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

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

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

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

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

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

Давайте начнем с простого компонента React, который отображает тег якоря (anchor tag):


link


<Link href="sid.studio">Click me</Link>

// будет отрендерено в:

<a href="sid.studio" class="link">Click me</a>

Вот как выглядит код компонента:


const Link = props => {
  return (
    <a href={props.href} className="link">
      {props.children}
    </a>
  )
}

Мы также хотим чтобы можно было добавлять к элементу такие html-атрибуты, как id, target, title, data-attr и т.д.


Поскольку существует много атрибутов HTML, мы можем просто передавать все пропсы, и добавить те что нам нужны (например className)


(Примечание: вы не должны передавать атрибуты, которые вы придумали для этого компонента, которых нет в спецификации HTML)


В этом случае можно просто использовать className


const Link = props => {
  /*
     мы используем троеточие (spread оператор), чтобы передать все
     свойства (включая дочерние)
  */
  return <a {...props} className="link" />
}

Вот где это становится интересным.


Кажется что все хорошо когда кто-то передает id или target:


<Link href="sid.studio" id="my-link">Click me</Link>

// будет отрендерено в:

<a  href="sid.studio" id="my-link" class="link">Click me</a>

но что происходит, когда кто-то передает className?


link


<Link href="sid.studio" className="red-link">Click me</Link>

// будет отрендерено в:

<a href="sid.studio" class="link">Click me</a>

Ну, ничего не произошло. React полностью проигнорировал пользовательский класс. Давайте вернемся к функции:


const Link = props => {
  return <a {...props} className="link" />
}

Хорошо, давайте представим как этот ...props компилируется, приведенный выше код эквивалентен этому:


const Link = props => {
  return (
    <a
      href="sid.studio"
      className="red-link"
      className="link"
    >
      Click me
    </a>
  )
}

Видите конфликт? Есть два пропа className. Как React справляется с этим?


Ну, React ничего делает. Babel делает!


Помните, что JSX "производит" React.createElement. Пропсы преобразуются в объект и передаются в качестве аргумента. Объекты не поддерживают дубликаты ключей, поэтому второй className перепишет первый.


const Link = props => {
  return React.createElement(
    'a',
    { className: 'link', href: 'sid.studio' },
    'Click me'
  )
}



Окей, теперь, когда мы знаем о проблеме, как нам ее решить?


Полезно понимать что эта ошибка возникла из-за конфликта имен, и это может произойти с любым пропом, а не только с className. Так что решение зависит от поведения которое вы хотите реализовать.


Есть три возможных сценария:


  1. Разработчик, использующий наш компонент, должен иметь возможность переопределить значение пропа по умолчанию
  2. Мы не хотим позволять разработчику менять некоторые пропсы
  3. Разработчик должен иметь возможность добавлять значения, при этом сохраняя значение по умолчанию

Давайте разбирать их по одному.


1. Разработчик, использующий наш компонент, должен иметь возможность переопределить значение пропа по умолчанию


Это поведение, которое вы обычно ожидаете от других атрибутов — id, title и т.д.


Мы часто видим настройку test id в cosmos (дизайн система, над которой я работаю). Каждый компонент получает data-test-id по умолчанию, иногда разработчики хотят вместо этого присоединить свой собственный идентификатор теста, чтобы обозначить определенное использование.


Вот один из таких вариантов использования:


breadcrumbs


const Breadcrumb = () => (
  <div className="breadcrumb" data-test-id="breadcrumb">
    <Link data-test-id="breadcrumb.link">Home</Link>
    <Link data-test-id="breadcrumb.link">Parent</Link>
    <Link data-test-id="breadcrumb.link">Page</Link>
  </div>
)

Breadcrumb использует ссылку, но вы хотите иметь возможность использовать ее в тестах с более конкретным data-test-id. В этом есть смыл.


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


На практике это означает, что пропсы по умолчанию должны идти первыми, а затем {...props} для их переопределения.


const Link = props => {
  return (
    <a className="link" data-test-id="link" {...props} />
  )
}

Помните, что второе появление data-test-id (из пропсов) переопределит первое (по умолчанию). Поэтому, когда разработчик присоединяет свой собственный data-test-id или className, он переопределяет тот который был по умолчанию:


1. <Link href="sid.studio">Click me</Link>
2. <Link href="sid.studio" data-test-id="breadcrumb.link">Click me</Link>

// будет отрендерено в:

1. <a class="link" href="sid.studio" data-test-id="link">Click me</a>
2. <a class="link" href="sid.studio" data-test-id="breadcrumb.link">Click me</a>

Мы можем сделать так и по отношению к className:


red-link


<Link href="sid.studio" className="red-link">Click me</Link>

// будет отрендерено в:

<a href="sid.studio" class="red-link" data-test-id="link">Click me</a>

Это выглядит довольно странно, я не уверен что мы должны допускать такое! Поговорим об этом дальше.


2. Мы не хотим позволять разработчику менять некоторые пропсы


Допустим, мы не хотим чтобы разработчики меняли внешний вид (через className), но мы не против что бы они меняли другие пропсы, такие как id, data-test-id и т.д.


Мы можем сделать это, упорядочив порядок наших атрибутов:


const Link = props => {
  return (
    <a data-test-id="link" {...props} className="link" />
  )
}

Помните, что атрибут справа будет переопределять атрибут слева. Таким образом, все до {...props} может быть переопределено, но все после него не может быть переопределено.


Чтобы упростить работу разработчикам, можно выводить предупреждение о том, что нельзя указывать свой className.


Мне нравится создавать собственные проверки типов пропсов для этого:


Link.PropTypes = {
  className: function(props) {
    if (props.className) {
      return new Error(
        `Недопустимый проп className для Link, этот компонент не допускает этой настройки`
      )
    }
  }
}

У меня есть видео, в котором рассказывается о проверке пользовательских типов пропсов, на случай если вам интересно как их писать.


Теперь, когда разработчик попытается переопределить className, это не сработает, а разработчик получит предупреждение.


link


<Link href="sid.studio" className="red-link">Click me</Link>

// будет отрендерено в:

<a href="sid.studio" class="link">Click me</a>

Внимание: Ошибка типа пропа:
Недопустимый проп className для Link, этот компонент не допускает этой настройки

Честно говоря, мне пришлось использовать этот шаблон только один или два раза. Обычно вы доверяете разработчику, использующему ваш компонент.


Что приводит нас к совместному использованию.


3. Разработчик должен иметь возможность добавлять значения, при этом сохраняя значение по умолчанию


Это, пожалуй, самый распространенный случай использования классов.


link


<Link href="sid.studio" className="underline">Click me</Link>

// будет отрендерено в:

<a href="sid.studio" class="link underline">Click me</a>

Реализация выглядит так:


const Link = props => {
  /* берем className используя деструктуризацию */
  const { className, otherProps } = props
  /* добавить классы по умолчанию */
  const classes = 'link ' + className

  return (
    <a
      data-test-id="link"
      className={classes}
      {...otherProps} /* передать все остальные пропсы
 */
    />
  )
}

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


switch


<Switch onClick={value => console.log(value)} />

Вот как выглядит реализация этого компонента:


class Switch extends React.Component {
  state = { enabled: false }

  onToggle = event => {
    /* сначала выполняем логику самого компонента */
    this.setState({ enabled: !this.state.enabled })

    /* потом вызвать обработчик событий из пропсов */
    if (typeof this.props.onClick === 'function') {
      this.props.onClick(event, this.state.enabled)
    }
  }

  render() {
    /*
      у нашего компонента уже есть обработчик кликов ️
    */
    return <div class="toggler" onClick={this.onToggle} />
  }
}

Есть другой способ избежать конфликта имен в обработчиках событий, его я описывал в Пишем API для React компонентов, часть 2: давайте названия поведению, а не способам взаимодействия.




Для каждого сценария вы можете использовать разные подходы.


  1. Большую часть времени: Разработчик должен иметь возможность изменить значение пропа, значение которого было задано по умолчанию
  2. Обычно для стилей и обработчиков событий: Разработчик должен иметь возможность добавить значение поверх значения по умолчанию
  3. Редкий случай, когда нужно ограничить действия разработчика: Разработчику не разрешено изменять поведение, нужно игнорировать его значения и, при этом, показывать предупреждения
Поделиться публикацией

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

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

    +1
    Даже не знаю что сказать, просто оставлю это здесь

    classnames

    const Link =({className, ...props}) => {
      return (
        <a {...props} className={className + ' link'} />
      )
    }
    
      0

      А можно вообще забыть про className и юзать styled-components.
      Статья, все таки, не про стили и классы, а про подходы.

        0
        но вы же приводите примеры именно на классах :(

        1. Разработчик, использующий наш компонент, должен иметь возможность переопределить значение пропа по умолчанию
        3. Разработчик должен иметь возможность добавлять значения, при этом сохраняя значение по умолчанию


        для этого можно использовать деструктуризацию из моего кода выше

        а вот если
        2. Мы не хотим позволять разработчику менять некоторые пропсы


        зачем вообще тогда принимать такие пропсы снаружи?
        больше смахивает на «Сначала мы производим грабли, а потом бьемся о них головой»
          0
          Эм, если что, это перевод.

          для этого можно использовать деструктуризацию из моего кода выше


          Можно, но смысл примеров в том что бы явно показать разные подходы.

          зачем вообще тогда принимать такие пропсы снаружи?


          Если честно, не знаю, возможно что бы явно показать разработчику что значение уже задано и выдавать ошибку если он попытается что-то изменить, то есть грабли подписаны и лежат намеренно.
            0
            Если честно, не знаю, возможно что бы явно показать разработчику что значение уже задано и выдавать ошибку если он попытается что-то изменить, то есть грабли подписаны и лежат намеренно.


            ну вот допустим вы подписали грабли атрибутом «color» и запретили его переопределять, а разработчик передает пропс «colors» — вы же не будете все гипотетические варианты указывать?

            Эм, если что, это перевод.

            Неважно Ваша ли это статья или тем более если это перевод, в данной статье нет ничего связанного с подходом, а тем более с «Пишем API для React компонентов», это всего лишь кусок из документации Typechecking With PropTypes
      0

      К теме не относится, но в последнем примере неправильно используется setState.В данном случае, было бы правильнее передавать в setState функцию, в которую прокидывается текущее значение state, а вторым параметром передавать callback, который гарантированно выполниться только после изменения state.


      this.setState(
        state => {
          const enabled = !state.enabled;
      
          return { enabled };
        },
        () => console.log(this.state.enabled)
      );

      В вашем примере на момент выполнения onClick значение state может быть еще не изменено.


      Спасибо за статью!

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

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