Пишем API для React компонентов, часть 1: не создавайте конфликтующие пропсы
Пишем API для React компонентов, часть 2: давайте названия поведению, а не способам взаимодействия
Пишем API для React компонентов, часть 3: порядок пропсов важен
Пишем API для React компонентов, часть 4: опасайтесь Апропакалипсиса!
Пишем API для React компонентов, часть 5: просто используйте композицию
Пишем API для React компонентов, часть 6: создаем связь между компонентами
Поговорим о формах.
Скорее всего вы читали кучу статей об управлении state
состояниями в формах, но это не одна из таких статей. Вместо этого я хочу поговорить о том как устроены формы и их API.
Здесь много чего происходит, взглянем на 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
, что полезно когда у вас мало места.
<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).