При написании сложных компонентов на React рано или поздно сталкиваешься с необходимостью обработки дочерних компонентов из родительского: передача дополнительных параметров, фильтрация компонентов по типу, удаление или добавление потомков и т.д. Рассмотрим, как это можно сделать на примере написания своего компонента.
Создадим компонент пошаговой инструкции. Подобный элемент интерфейса активно используется на портале Госуслуг.

Пошаговая инструкция на портале Госуслуги.
В простейшем случае компонент представляет собой список (StepsList) последовательно пронумерованных шагов (StepItem), каждый из которых состоит из заголовка (StepTitle) и описания шага (StepDescription). Использование компонента будет выглядеть, как показано ниже.
<StepsList>
<StepItem>
<StepTitle>Шаг 1</StepTitle>
<StepDescription>Описание шага 1</StepDescription>
</StepItem>
<StepItem>
<StepTitle>Шаг 2</StepTitle>
<StepDescription>Описание шага 2</StepDescription>
</StepItem>
</StepsList>
Опишем все необходимые части компонента. Первые три компонента достаточно простые.
Файл StepTitle.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
export default class StepTitle extends Component {
render() {
return (
<h3 className="step-title">
{this.props.children}
</h3>
);
}
};
Файл StepDescription.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
export default class StepDescription extends Component {
render() {
return (
<div className="step-description">
{this.props.children}
</div>
);
}
};
Файл StepItem.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
export default class StepItem extends Component {
render() {
return (
<div className="step">
<div className={"step-number " + this.props.CSSClasses}>
{this.props.stepLabel}
</div>
<div className="step-container">
<div className="step-content">
{this.props.children}
</div>
</div>
</div>
);
}
};
Так как шаги нумеруются последовательно, номер шага будем формировать автоматически, а затем передавать компоненту StepItem через props. Делать это логично в родительском компоненте StepsList, т.к. он может иметь доступ ко всем шагам и знать их порядок.
Нам понадобится функция перебора массива дочерних компонентов. В React для этого есть метод React.Children.map()
, возвращающий новый массив. Этот метод полезен тем, что корректно обрабатывает ситуацию, когда дочерний элемент всего один, и вместо массива в this.props.children содержится сам элемент.
React.Children.map(this.props.children, (child, index) => {
return child;
})
Далее нам нужно отфильтровать все элементы внутри StepsList, не являющиеся экземпляром StepItem.
if (React.isValidElement(child) && child.type === StepItem) {}
Метод React.isValidElement(elem)
проверяет, является ли компонент компонентом React. Свойство child.type содержит класс, экземпляром которого является компонент.
Когда мы убедились, что текущий дочерний элемент - это StepItem, нужно добавить ему дополнительное свойство с номером шага. Поскольку мы не можем изменять свойства текущего элемента (свойство props доступно только для чтения), склонируем элемент с добавлением новых свойств.
React.cloneElement(child, {
newprop: value,
})
Таким образом в массив childrenWithProps попадут только компоненты определённого типа, весь прочий код будет проигнорирован.
Файл StepsList.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
import StepItem from './StepItem';
export default class StepsList extends Component {
render() {
const childrenWithProps = React.Children.map(this.props.children,
(child, index) => {
if (React.isValidElement(child) && child.type === StepItem) {
return React.cloneElement(child, {
stepLabel: index+1,
})
}
}
);
return (
<div className="instruction">
{childrenWithProps}
</div>
);
}
};
Усложним немного код, добавив возможность создавать специальные шаги, помечаемые не номером, а каким-нибудь значком. Например, чтобы обозначить первый и последний шаг. поскольку наши “специальные” шаги не будут иметь номера, добавим переменную offset, чтобы нумерация не сбивалась.
Также будем назначать таким шагам определённые css-классы, чтобы при необходимости выделить их стилями. Количество блоков if соответствует количеству различный типов шагов, у нас их два: шаг предусловий и финальный шаг. При этом шаг не может одновременно принадлежать к двум типам.
Файл StepsList.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
import StepItem from './StepItem';
export default class StepsList extends Component {
render() {
let offset = 1;
const childrenWithProps = React.Children.map(this.props.children,
(child, index) => {
if (React.isValidElement(child) && child.type === StepItem) {
let label = index+offset;
let class = "";
if (child.props.precondition) {
label = "\u25CF"; // see: Numeric character reference
class = "step-precondition";
offset--;
}
if (child.props.final) {
label = "\u2714";
class = "step-final";
}
return React.cloneElement(child, {
stepLabel: label,
CSSClasses: class
})
}
}
);
return (
<div className="instruction">
{childrenWithProps}
</div>
);
}
};
Теперь при создании инструкции мы можем задавать шагам тип. В случае с первым и последним шагом тип им можно было задавать автоматически, но выбранный подход работает в общем случае с любым шагом даже из середины списка. Для каких-то шагов можно не делать пропуск (offset--;
), и считать их тоже.
Файл Instruction.jsx
import ReactDOM from 'react-dom';
import React, {Component} from 'react';
import StepItem from './StepList';
import StepItem from './StepItem';
import StepItem from './StepTitle';
import StepItem from './StepDescription';
export default class Instruction extends Component {
render() {
return (
<StepsList>
<StepItem precondition>
<StepTitle>Шаг 1</StepTitle>
<StepDescription>Описание шага 1</StepDescription>
</StepItem>
<StepItem>
<StepTitle>Шаг 2</StepTitle>
<StepDescription>Описание шага 2</StepDescription>
</StepItem>
<StepItem final>
<StepTitle>Шаг 3</StepTitle>
<StepDescription>Описание шага 3</StepDescription>
</StepItem>
</StepsList>
);
}
};
Добавим немного стилей.
Файл style.css
.instruction {
margin-top: 20px;
}
.step:last-of-type .step-container {
border-color: transparent;
}
.step .step-number {
display: inline-block;
width: 30px;
height: 30px;
background: #fff;
border-radius: 15px;
line-height: 30px;
text-align: center;
float: left;
color: #333;
font-size: 13px;
border: 2px solid #1f9b90;
box-sizing: border-box;
}
.step .step-container {
margin-left: 15px;
padding: 0px 10px 10px 30px;
border-left: 2px solid #1f9b90;
max-width: 600px;
}
.step .step-content {
background: #C6E9E6;
border-radius: 5px;
padding: 15px;
color: #333;
font-size: 14px;
position: relative;
}
.step .step-content:before {
display: inline-block;
content: "";
border: 10px solid transparent;
border-right: 10px solid #C6E9E6;
position: absolute;
right: 100%;
top: 5px;
}
.step .step-title {
margin: 0px 0px 10px 0px;
}
.instruction .step-precondition,
.instruction .step-final {
font-size: 14px;
color: #fff;
background: #1f9b90;
}
Вид получившегося компонента в действии:
