React хорош тем, что позволяет делать композицию чего угодно. И есть не один способ добиться желаемого. Рассмотрим мой любимый: паттерн render-props. Он похож на hoc, но для меня субъективно удобнее и читаемее. Хотя всё, конечно, зависит от конкретной задачи. Итак...
Задача: реализовать группы чекбоксов, каждая со своим изолированным состоянием. Стилизация одного чекбокса зависит от состояния другого. Если выделены несколько элементов подряд, между ними нужно отрисовать линию, их соединяющую.
Решение: создать два компонента. Первым из которых будет сам чекбокс. Вторым — группа чекбоксов. Группа будет знать всё обо всех чекбоксах внутри себя и ничего о чекбоксах соседней группы.
Выглядит наша задумка следующим образом:
Приступим. Возьмём create-react-app
в качестве каркаса приложения
и classnames
для стилизации.
$ npx create-react-app form-app
$ cd form-app
$ yarn add classnames
Структура приложения:
$ tree -L 3
.
├── package.json
├── public
├── src
│ ├── App.js
│ ├── components
│ │ ├── checkbox
│ │ │ ├── checkbox.css
│ │ │ └── checkbox.js
│ │ ├── checkbox-group
│ │ │ ├── checkbox-group.css
│ │ │ └── checkbox-group.js
│ │ └── index.js
│ └── index.js
└── yarn.lock
Для удобства импорт всех компонентов можно организовать из одного файла.
// components/index.js
import CheckBox from './checkbox/checkbox';
import CheckboxGroup from './checkbox-group/checkbox-group';
export {
CheckBox,
CheckboxGroup,
}
Идём от малого к большому. Сначала сам чекбокс.
import React, { useState, memo } from 'react';
import './checkbox.css';
const classNames = require('classnames');
const CheckBox = props => {
const { name, label, isInGroup, isDisabled, handleChange } = props;
// состояние чекбокса: отмечен или нет
// сделаем так, чтобы начальное значение можно было передать извне
const [isChecked, setIsChecked] = useState(props.isChecked);
// изменение состояния
// и вызов внешней функции handleChange при необходимости
const toggleCheckbox = () => {
if (isDisabled) return;
handleChange({ isChecked: !isChecked, label, name });
setIsChecked(!isChecked);
};
// динамическая стилизация
// если isInGroup, то добавить класс-модификатор
const checkboxClass = classNames({
'checkbox': true,
'checkbox_inGroup': isInGroup,
});
return (
<label className="label">
<input
type="checkbox"
className={checkboxClass}
name={name}
label={label}
checked={!!isChecked}
onChange={toggleCheckbox} />
{name}
</label>
);
};
CheckBox.displayName = 'CheckBox';
export default memo(CheckBox);
Осторожно
Нужно быть аккуратным с render-props: предостережение.
Стилизация не относится к теме render-props'ов и для сокращения кода пропускается.