javascript

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

  • воскресенье, 14 июля 2019 г. в 00:25:43
https://habr.com/ru/post/459422/
  • JavaScript
  • ReactJS


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

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

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

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

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

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

Поговорим о формах.


Скорее всего вы читали кучу статей об управлении state состояниями в формах, но это не одна из таких статей. Вместо этого я хочу поговорить о том как устроены формы и их API.


label-on-left


Здесь много чего происходит, взглянем на API


<Form layout="label-on-left">
  <Form.Field label="Name">
    <TextInput type="text" placeholder="Enter your name" />
  </Form.Field>

  <Form.Field label="Email">
    <TextInput
      type="email"
      placeholder="email@domain.com"
    />
  </Form.Field>
</Form>

Давайте посмотрим на каждый из компонентов и разберем их:


Форма


Все начинается с компонента Form, который является базовым элементом формы с прикрепленным классом. Он будет рендерить все что вы положите в него.


function Form(props) {
  return <form className="form">{props.children}</form>
}

render(<Form layout="label-on-left">...</Form>)

Он также принимает проп layout, что полезно когда у вас мало места.


label-on-top-phone


<Form layout="label-on-top">...</Form>

Это меняет способ выравнивания надписей (справа налево) и то как работает margin.


Форма не контролирует ширину и margin своего внутреннего содержимого. Это уже забота для самого поля ввода находящегося внутри этой формы.


Так что компонент Form должен сообщать информацию о layout ниже.


Проще всего было бы передать layout при помощи пропсов, но содержимое формы является динамическим (определяется разработчиком, который использует эту форму), мы точно не знаем какой будет форма.


Вот где нам поможет контекстный API.


/* Создаем новый контекст */
const LayoutContext = React.createContext()

function Form(props) {
  /*
    Оборачиваем дочерние элементы
    в `Provider` контекста
    со значением основанным на пропсах
  */
  return (
    <form className="form">
      <LayoutContext.Provider
        value={{ layout: props.layout }}
      >
        {props.children}
      </LayoutContext.Provider>
    </form>
  )
}

export default Form
export { LayoutContext }

Теперь поле формы может использовать этот контекст и получить значение layout


Поле формы


Компонент FormField (поле ввода формы), добавляет label ко всему что вы помещаете в него (например, текстовый ввод).


function Field(props) {
  return (
    <div className="form-field">
      <label {...props}>{props.label}</label>
      {props.children}
    </div>
  )
}

В дополнение к этому, он добавляет класс для layout — который приходит из контекста, который мы создали в компоненте Form.


/* Получаем потребителя layout */
import { LayoutContext } from './form'

/*

  Используем потребителя для того что бы
  получить доступ к контексту - он использует
  рендер проп API (Render Prop API)

  Мы передаем это как класс в поле формы
*/
function Field(props) {
  return (
    <LayoutContext.Consumer>
      {context => (
        <div className={`form-field ${context.layout}`}>
          <label {...props}>{props.label}</label>
          {props.children}
        </div>
      )}
    </LayoutContext.Consumer>
  )
}

Хук useContext из React 16.8+ облегчает понимание синтаксиса


/* Получаем потребителя layout */
import { LayoutContext } from './form'

function Field(props) {
  /*
    Берем контекст из useContext хука
    который принимает переменную контекста
    в качестве входных данных
  */
  const context = useContext(LayoutContext)

  return (
    <div className={`form-field ${context.layout}`}>
      <label {...props}>{props.label}</label>
      {props.children}
    </div>
  )
}

Если вам интересно, вот css код:


.form-field.label-on-left {
  max-width: 625px;
  display: flex;
  align-items: center; /* выровнять по вертикали */
}
.form-field.label-on-left label {
  text-align: right;
  width: 175px;
  margin-right: 25px;
}

.form-field.label-on-top {
  width: 100%;
  display: block; /* вместо flex*/
}
.form-field.label-on-top label {
  text-align: left; /* вместо right */
  margin-bottom: 25px; /* вместо margin-right */
}

Form.Field?


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


Поскольку Field (поле ввода) всегда используется с формой, есть смысл в том что бы сгруппировать их вместе.


Один из способов сделать это — экспортировать его из того же файла:


/* form.js */
import Field from './field'

function Form(props) {
  /* ... */
}
export default Form

export { Field }

И теперь пользователи могут импортировать их вместе:


import Form, { Field } from 'components/form'

render(
  <Form>
    <Field>...</Field>
  </Form>
)

Мы можем сделать небольшое улучшение, прикрепив Field к самому компоненту формы.


/* form.js */
import Field from './field'

function Form(props) {
  /* ... */
}

Form.Field = Field
export default Form

Этот код работает потому что React компоненты являются javascript объектами, и вы можете добавлять дополнительные ключи к этим объектам.


Для пользователя это означает, что когда он импортирует Form (форму), он получает Field (поле) автоматически.


import Form from 'components/form'

render(
  <Form>
    <Form.Field>...</Form.Field>
  </Form>
)

Мне очень нравится этот API, он делает связь между Form и Form.Field очевидной.


Примечание: вы должны переместить контекст в другой файл, чтобы избежать циклической зависимости.


Сочетание синтаксиса с точками и контекста делает наш компонент Form умным, при этом сохраняя его работоспособность для композиций (composite).