По прошествии 2-х лет работы с React у меня накопилось немного опыта, которым хотелось бы поделиться. Если вы только начали осваивать React, то надеюсь эта статья поможет выбрать правильный путь развития проекта из 1-5 форм до огромного набора компонентов и при этом не запутаться.
Если вы уже профи, то, возможно, вспомните свои костыли. А возможно предложите более качественные решения описанных проблем.
В этой статье речь пойдет о моем личном мнении как организовать композицию компонентов.
Рассмотрим некоторую абстрактную форму. Будем подразумевать что полей в форме много (порядка 10-15 штук), но, чтобы глаза не разбегались, в качестве примера возьмем форму с 4-мя полями.
На вход в компонент прилетает многоуровневый объект такого вида:
Неопытный разработчик (как я в первый месяц работы с реактом) сделает все это в одном компоненте где в стейте будут храниться значения инпутов:
Увидев, как быстро разработчик справился, заказчик предложит сделать на основе этой формы еще одну, но чтобы было без блока “size”.
Вариантов видится 2 (и оба не правильные):
Если после реализации 3-5 форм проект окончен, то разработчику повезло.
Но обычно это только начало, и количество разных формочек только растет.
Потом требуется похожая, но без блока “color”.
Потом похожая, но с новым блоком “description”.
Потом какие-то блоки сделать только для чтения.
Потом похожую форму надо вставить в другую форму — вообще печаль, что из этого иногда получается.
Разработчик, выбравший подход с копированием, конечно быстро справится с реализацией новых форм. Пока их будет меньше 10. Но потом настроение будет постепенно падать.
Особенно когда случается редизайн. Отступы там между блоками формы подправить «немного», компонент выбора цвета поменять. Ведь все сразу предусмотреть нельзя и многие дизайнерские решения придется пересматривать после их воплощения в жизнь.
Тут важно обратить внимание на частое упоминание “похожую форму”. Ведь продукт один и все формочки должны быть похожие. В итоге приходится заниматься очень неинтересной и рутинной работой по переделке одного и того же в каждой форме, и тестировщикам кстати тоже надо будет каждую форму перепроверить.
Если разработчик выбрал второй путь, то конечно же тут он на коне — подумаете вы. У него всего несколько компонентов которыми можно нарисовать несколько десятков форм. Подправить по всему проекту отступ или поменять компонент “color” — это подправить две строчки в коде и тестировщику надо будет перепроверить всего в парочке мест.
Но на самом деле этот путь породил очень сложный в поддержке компонент.
Пользоваться им сложно, т.к. множество параметров, некоторые называются почти одинаково, чтобы понять за что отвечает каждый параметр надо лезть во внутренности.
Поддерживать тоже сложно. Как правило внутри сложные переплетения условий и добавление нового условия может сломать все остальное. Подправив компонент для вывода одной формы могут сломаться остальные.
Например делают параметр fields (ну как columns в react-table). И туда передают параметры полей: какое поле видимо, какое не редактируемое, имя поля.
Вызов компонента превращается в такой:
В итоге разработчик горд собой. Он обобщил настройки всех полей и оптимизировал внутренний код компонента: теперь для каждого поля вызывается одна функция, которая преобразует конфигурацию в пропсы соответствующего компонента. Даже по имени типа рендерит разный компонент. Еще немного и получится свой фреймворк.
Круто? Уж слишком.
Надеюсь это не превратится в такое:
Давайте вспомним на чем мы все таки пишем. У нас уже есть библиотека react. Не надо выдумывать никаких новых конструкций. Конфигурация компонентов в реакте описывается с помощью разметки JSX
Кажется мы вернулись к первому варианту с копированием. Но на самом деле нет. Это композиция которая избавляет от проблем первых двух подходов.
Есть набор кирпичиков из которых собирается форма. Каждый кирпичик отвечает за что-то свое. Какой-то за лайоут и внешний вид, какой-то за ввод данных.
Если нужно изменить отступы во всем проекте, то это достаточно сделать в компоненте FormField. Если нужно поменять работу выпадающего списка, то это делается в одном месте в компоненте DropDown.
Если нужна похожая форма но например чтобы не было поля “color”, то выносим общие блоки в отдельные кирпичики и собираем другую форму.
Выносим блок Size в отдельный компонент:
Делаем форму с выбором цвета:
Делаем похожую форму, но без выбора цвета:
Самое главное, человеку, которому достанется такой код, не нужно разбираться с выдуманными конфигами предшественника. Все написано на знакомом любому react-разработчику JSX с подсказками параметров каждого компонента.
Теперь обратим внимание на стейт. Точнее на его отсутствие. Как только мы добавим стейт, мы замкнем поток данных и переиспользовать компонента станет сложнее. Все кирпичики должны быть stateless (т.е. без стейта). И только на самом верхнем уровне собранную из кирпичиков форму можно подключать к стейту. Если форма сложная, то тут уже есть смысл разделить ее на несколько контейнеров и подконектить каждую часть к redux.
Не ленитесь делать отдельно stateless компонент формы. Тогда у вас будет возможность использовать его как часть другой формы, либо сделать на его основе statefull форму либо контейнер для подключения к redux.
Конечно в кирпичиках могут быть стейты для хранения внутреннего состояния не связанного с общим потоком данных. Например во внутреннем стейте DropDown (выпадающий список) удобно хранить признак раскрыт он или нет.
Как не удивительно, я периодически сталкиваюсь со всеми описанными в статье ошибками и проблемами которые из них вытекают. Надеюсь Вы не будете их повторять и тогда поддержка вашего кода станет намного проще.
Повторю основные тезисы:
Если вы уже профи, то, возможно, вспомните свои костыли. А возможно предложите более качественные решения описанных проблем.
В этой статье речь пойдет о моем личном мнении как организовать композицию компонентов.
Начнем с малого
Рассмотрим некоторую абстрактную форму. Будем подразумевать что полей в форме много (порядка 10-15 штук), но, чтобы глаза не разбегались, в качестве примера возьмем форму с 4-мя полями.
На вход в компонент прилетает многоуровневый объект такого вида:
const unit = {
name: 'unit1',
color: 'red',
size: {
width: 2,
height: 4,
},
}
Неопытный разработчик (как я в первый месяц работы с реактом) сделает все это в одном компоненте где в стейте будут храниться значения инпутов:
const Component = ({ values, onSave, onCancel }) => {
const [ state, setState ] = useState({});
useEffect(() => {
setState(values);
}, [ values, setState ]);
return <div className="form-layout">
<div className="form-field">
<Input onChange={({ target: { value } }) =>
setState((state) => ({...state, name: value }))
}/>
</div>
<div className="form-field">
<Input onChange={({ target: { value } }) =>
setState((state) => ({...state, color: value }))
}/>
</div>
<div className="size">
<div className="form-field">
<Input onChange={({ target: { value } }) =>
setState((state) => ({...state, size: { width: value } }))
}/>
</div>
<div className="form-field">
<Input onChange={({ target: { value } }) =>
setState((state) => ({...state, size: { height: value } }))
}/>
</div>
</div>
<div className="buttons">
<Button onClick={() => onSave(state)}>Save</Button>
<Button onClick={() => onCancel()}>Cancel</Button>
</div>
</div>
}
Увидев, как быстро разработчик справился, заказчик предложит сделать на основе этой формы еще одну, но чтобы было без блока “size”.
Вариантов видится 2 (и оба не правильные):
- Можно скопировать первый компонент и добавить туда то, чего не хватает или убрать лишнее. Такое обычно делают когда за основу берут не свой компонент и боятся что-то в нем сломать
- Добавить в параметры дополнительные настройки компонента.
Если после реализации 3-5 форм проект окончен, то разработчику повезло.
Но обычно это только начало, и количество разных формочек только растет.
Потом требуется похожая, но без блока “color”.
Потом похожая, но с новым блоком “description”.
Потом какие-то блоки сделать только для чтения.
Потом похожую форму надо вставить в другую форму — вообще печаль, что из этого иногда получается.
Новые формы путем копирования
Разработчик, выбравший подход с копированием, конечно быстро справится с реализацией новых форм. Пока их будет меньше 10. Но потом настроение будет постепенно падать.
Особенно когда случается редизайн. Отступы там между блоками формы подправить «немного», компонент выбора цвета поменять. Ведь все сразу предусмотреть нельзя и многие дизайнерские решения придется пересматривать после их воплощения в жизнь.
Тут важно обратить внимание на частое упоминание “похожую форму”. Ведь продукт один и все формочки должны быть похожие. В итоге приходится заниматься очень неинтересной и рутинной работой по переделке одного и того же в каждой форме, и тестировщикам кстати тоже надо будет каждую форму перепроверить.
В общем, вы поняли. Не копируйте похожие компоненты.
Новые формы путем обобщения
Если разработчик выбрал второй путь, то конечно же тут он на коне — подумаете вы. У него всего несколько компонентов которыми можно нарисовать несколько десятков форм. Подправить по всему проекту отступ или поменять компонент “color” — это подправить две строчки в коде и тестировщику надо будет перепроверить всего в парочке мест.
Но на самом деле этот путь породил очень сложный в поддержке компонент.
Пользоваться им сложно, т.к. множество параметров, некоторые называются почти одинаково, чтобы понять за что отвечает каждый параметр надо лезть во внутренности.
<Component
isNameVisible={true}
isNameDisabled={true}
nameLabel="Model"
nameType="input"
isColorVisible={true}
isColorDisabled={false}
colorType={'dropdown'}
isSizeVisible={true}
isHeightVisible={true}
isWidthDisabled={false}
/>
Поддерживать тоже сложно. Как правило внутри сложные переплетения условий и добавление нового условия может сломать все остальное. Подправив компонент для вывода одной формы могут сломаться остальные.
В общем, вы поняли. Не делайте компоненту много свойств.Чтобы решить проблемы второго варианта разработчики начинают что? Правильно. Как настоящие разработчики они начинают разрабатывать нечто, что упрощает настройку сложного компонента.
Например делают параметр fields (ну как columns в react-table). И туда передают параметры полей: какое поле видимо, какое не редактируемое, имя поля.
Вызов компонента превращается в такой:
const FIELDS = {
name: { visible: true, disabled: true, label: 'Model', type: 'input' },
color: { visible: true, disabled: false, type: 'dropdown' },
size: { visible: true },
height: { visible: true },
width: { disabled: false },
}
<Component
values={values}
fields={FIELDS}
/>
В итоге разработчик горд собой. Он обобщил настройки всех полей и оптимизировал внутренний код компонента: теперь для каждого поля вызывается одна функция, которая преобразует конфигурацию в пропсы соответствующего компонента. Даже по имени типа рендерит разный компонент. Еще немного и получится свой фреймворк.
Круто? Уж слишком.
Надеюсь это не превратится в такое:
const FIELDS = {
name: getInputConfig({ visible: true, disabled: true, label: 'Model'}),
color: getDropDownConfig({ visible: true, disabled: false}),
size: getBlockConfig({ visible: true }),
height: getInputNumberConfig({ visible: true }),
width: getInputNumberConfig({ disabled: false }),
}
<Component
values={values}
fields={FIELDS}
/>
В общем, вы поняли. Не изобретайте велосипед.
Новые формы путем композиции компонентов и вложенных форм
Давайте вспомним на чем мы все таки пишем. У нас уже есть библиотека react. Не надо выдумывать никаких новых конструкций. Конфигурация компонентов в реакте описывается с помощью разметки JSX
const Form1 = ({ values }) => {
return <FormPanel>
<FormField disabled label=”Model”>
<Input name="name" />
</FormField>
<FormField disabled label=”Color”>
<DropDown name="color" />
</FormField>
<FormPanel>
<FormField disabled label="Height">
<Input.Number name="height" />
</FormField>
<FormField disabled label="Width">
<Input.Number name="width" />
</From Field>
</FormPanelt>
</FormPanel>
}
Кажется мы вернулись к первому варианту с копированием. Но на самом деле нет. Это композиция которая избавляет от проблем первых двух подходов.
Есть набор кирпичиков из которых собирается форма. Каждый кирпичик отвечает за что-то свое. Какой-то за лайоут и внешний вид, какой-то за ввод данных.
Если нужно изменить отступы во всем проекте, то это достаточно сделать в компоненте FormField. Если нужно поменять работу выпадающего списка, то это делается в одном месте в компоненте DropDown.
Если нужна похожая форма но например чтобы не было поля “color”, то выносим общие блоки в отдельные кирпичики и собираем другую форму.
Выносим блок Size в отдельный компонент:
const Size = () => <FormPanel>
<FormField disabled label="Height">
<Input.Number name="height" />
</FormField>
<FormField disabled label=”Width”>
<Input.Number name="width" />
</From Field>
</FormPanel>
Делаем форму с выбором цвета:
const Form1 = () => <FormPanel>
<FormField disabled label="Color">
<DropDown name="color" />
</FormField>
<FormField disabled label="Model">
<Input name="name" />
</FormField>
<Size name="size" />
</FormPanel>
Делаем похожую форму, но без выбора цвета:
const Form2 = () => <FormPanel>
<FormField disabled label="Model">
<Input name="name" />
</FormField>
<Size name="size" />
</FormPanel>
Самое главное, человеку, которому достанется такой код, не нужно разбираться с выдуманными конфигами предшественника. Все написано на знакомом любому react-разработчику JSX с подсказками параметров каждого компонента.
В общем, вы поняли. Используйте JSX и композицию из компонентов.
Несколько слов по поводу State
Теперь обратим внимание на стейт. Точнее на его отсутствие. Как только мы добавим стейт, мы замкнем поток данных и переиспользовать компонента станет сложнее. Все кирпичики должны быть stateless (т.е. без стейта). И только на самом верхнем уровне собранную из кирпичиков форму можно подключать к стейту. Если форма сложная, то тут уже есть смысл разделить ее на несколько контейнеров и подконектить каждую часть к redux.
Не ленитесь делать отдельно stateless компонент формы. Тогда у вас будет возможность использовать его как часть другой формы, либо сделать на его основе statefull форму либо контейнер для подключения к redux.
Конечно в кирпичиках могут быть стейты для хранения внутреннего состояния не связанного с общим потоком данных. Например во внутреннем стейте DropDown (выпадающий список) удобно хранить признак раскрыт он или нет.
В общем, вы поняли. Разделяйте компоненты на Stateless и Statefull.
Итого
Как не удивительно, я периодически сталкиваюсь со всеми описанными в статье ошибками и проблемами которые из них вытекают. Надеюсь Вы не будете их повторять и тогда поддержка вашего кода станет намного проще.
Повторю основные тезисы:
- Не копируйте похожие компоненты. Используйте принцип DRY.
- Не делайте компоненты с большим количеством свойств и функционала. Каждый компонент должен отвечать за что-то свое (Single Responsibility из SOLID)
- Разделяйте компоненты на Stateless и Statefull.
- Не изобретайте свои конструкции. Используйте JSX и композицию из своих компонентов.