Композиция компонентов в React

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'ов и для сокращения кода пропускается.