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

Автор оригинала: Sid
  • Перевод
  • Tutorial
Пишем 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).

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

    +1

    Отдельно Field всё равно экспортировать надо, потому что он может быть вынесен в отдельный файл, где Form не нуєжен

      0
      Хорошая практика — везде использовать именованные экспорты / импорты. Вместе import Field from './field' лучше использовать import { Field } from './Field'. Иначе готовьтесь к Fild / Feld и т.п. переменным по недосмотру.
        0
        What you gonna do когда у тебя на одной страницы будет 2 таких разных формы которых объединяет 1 общий контекст, когда он должен быть изолирован для каждой?
          0
          Видимо, напишет еще одну часть, в которой будут новые глупые ошибки
            0

            Контексты работают достаточно изолированно для большинства кейсов. Вложенные провайдеры одного контекста образуют стэк значений, невложенные — параллельные стэки: https://codesandbox.io/s/quirky-hill-txjny
            Вроде нет способа получить внутри провайдера данные того же контекста его родителей или соседей

              0
              Ну тогда ещё ладно, но все равно, эта лапша выглядит не очень) Более компактный бы вариант такого проброса
                0

                C хуком более компакто, ну и небольшой "хак" с провайдером https://codesandbox.io/s/thirsty-lake-36kd8 :)

                  0
                  Уже другое дело, но я например не фанат хуков и функциональщины, мне бы это все добро в классе и с Mobx'ом
                    +1
                    Например вот такой HOC =) codesandbox.io/s/purple-sky-x5nxq

                    P.S. исправил уже без HOC, там оказывается все намного проще если посмотреть в документацию))
                      0

                      Один раз — не… Я тоже не фанат, но нам или так, или оборачивать классы в примитивные HOC, или громоздкие консьюмеры. На практике HOC предпочту, если какие-то универсальные провайдеры, а по мелочи — hook

                        0
                        Надо сделать обвязку чтобы двухсторонний обмен был)
                          +1
                          Двух сторонний обмен по контексту с MobX'ом ) codesandbox.io/s/frosty-snyder-lkz3q
                    0
                    VolCh, а есть ли какие-то минусы (память, производительсность ...) если абсолютно все провайдеры держать на самом верхнем уровне оборачивая ими всё приложение?
                    Типа вот так codesandbox.io/embed/nice-hooks-wui5t

                    Вроде как удобно, всё в кучке. Любой компонент, на любом уровне вложенности (только в HOC его обернуть), имеет доступ к любому контексту. Или всё же желательно провайдер держать как можно ближе только к общим потребетилям?
                      0

                      Абсолютно все во многих случаях не получится в принципе из-за стэковой природы контекстов. Вот тот же подход к формам, описанный в посте (используется во многих библиотеках реакт-компонентов для работы с формами): сложновато будет, как минимум, пробросить в контекст все данные для всех форм приложения за один раз.


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


                      Минусы по факту есть: ресурсы контексты потребляют, конечно больше, чем проброс свойств на один уровень, но если не создавать их тысячами, то вряд ли заметите. Тут больше не про ресурсы, а про поддержку. Глобальные контексты — по сути те же глобальные переменные со всеми вытекающими, особенно если прокидывают методы для изменения своего состояния.


                      На практике на уровне приложения больше 5 провайдеров на уровне всего приложения создавать не приходилось: глобальные роуты, глобальный стор, темизация, авторизация, логирование или типа того. Может ещё 1 или 2, но больше ничего вспомнить не получается, хотя сейчас одна задача есть — может быть внедрим через контекст Di-контейнер или сервис-локатор, чтобы избавиться от импортов, как минимум, конкретных объектов сервисов. Но ещё исследования не закончили.

                        0
                        Спасибо.

                        но если не создавать их тысячами

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

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

                        Да, всё так. Контексты создаю и разделяю по фичам, которые размазаны по всему приложению. А локальные фичи — соответственно в стейте умного компонента этой фичи. Другое дело, что нужно поймать момент, когда фича перестаёт быть локальной, и её нужно выносить в контекст..)

                        Глобальные контексты — по сути те же глобальные переменные

                        Этот вопрос решаю именованием контекста именем фичи, за которую он отвечает.
                          0

                          Могут быть и локальные контексты, как формы в посте.


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

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

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