Погружаемся в работу с children на React
В этой статье мы погрузимся в работу с children
на React и узнаем как можно с ними работать на реальных примерах.
Основа React это компоненты. Вы можете вкладывать компоненты друг в друга также, как вы вкладываете друг в друга HTML теги, что упрощает работу с JSX, который собственно и напоминает HTML.
Когда я впервые увидел React, то я подумал, что нужно просто использовать props.children
и дело в шляпе. Но как же сильно я ошибался.
Так как мы работаем с JavaScript, то мы можем делать с дочерними элементами (children
) буквально всё, что угодно. Мы можем смело задавать им конкретные свойства, решать нужно ли их рендерить, ну и конечно же, управлять ими так, как нам надо. Так что дело это полезное и уж давайте при случае как можно глубже познаем всю прелесть работы с children
на React.
Поехали!
Дочерние компоненты
Предположим, что у нас есть компонент <Grid />
, который может содержать в себе компоненты <Row />
. Мы бы написали это так:
<Grid>
<Row />
<Row />
<Row />
</Grid>
Эти три компонента Row
передаются компоненту Grid
как props.children
. Используя контейнер для выражения (expression container это технический термин для этих волнистых скобок в JSX) родители могут рендерить свои дочерние элементы:
class Grid extends React.Component {
render() {
return <div>{this.props.children}</div>
}
}
Родительские элементы также могут и не рендерить своих потомков или могут производить с ними какие-либо действия перед рендерингом. Для примера, этот компонент. <Fullstop />
вообще не будет рендерить свои дочерние элементы:
class Fullstop extends React.Component {
render() {
return <h1>Hello world!</h1>
}
}
Совершенно неважно какие дочерние элементы будут переданы этому компоненту, он просто будет всегда показывать "Hello world!"
и ничего более.
Обратите внимание, что h1
в примере выше рендерит свои дочерние элементы, в нашем случае это "Hello world!"
.
Дочерним элементом может быть все, что угодно
Children
в React не обязательно быть компонентами, они могут быть всем, чем угодно. Для примера, мы можем передать нашему вышеупомянутому компоненту <Grid />
какой-либо текст в виде children
и всё будет прекрасно работать:
<Grid>Hello world!</Grid>
JSX автоматически удалит пробелы вначале и конце строки, а вместе с ними и пустые строки. Также пустые строки в середине строкового литерала преобразуются в один пробел.
Это означает то, что все эти примеры ниже отрендерят одно и тоже:
<Grid>Hello world!</Grid>
<Grid>
Hello world!
</Grid>
<Grid>
Hello
world!
</Grid>
<Grid>
Hello world!
</Grid>
Вы также можете смежно использовать разные типы дочерних элементов:
<Grid>
Here is a row:
<Row />
Here is another row:
<Row />
</Grid>
Функция как дочерний элемент
Мы можем передать любое JavaScript выражение, как дочерний элемент. И функции не исключение.
Чтобы показать то, как это выглядит, посмотрите на этот компонент, который выполняет функцию, которая была передана как дочерний элемент:
class Executioner extends React.Component {
render() {
// Обратите внимание на то, как мы вызываем дочерний элемент, как функцию?
// ↓
return this.props.children()
}
}
Вы бы могли вызвать этот компонент так:
<Executioner>
{() => <h1>Hello World!</h1>}
</Executioner>
Это конечно не совсем пригодный пример, но он показывает саму идею.
Представьте, что вам надо подтянуть какие-либо данные с сервера. Вы могли бы сделать это несколькими способами и это возможно сделать с этим function-as-a-child паттерном:
<Fetch url="api.myself.com">
{(result) => <p>{result}</p>}
</Fetch>
Не переживайте, если код выше смутил вас. Все, что я хотел сделать это то, чтобы вы не удивились увидев нечто подобное в реальной жизни. С children
возможно всё.
Управление children
Если вы посмотрите на документацию React, то вы увидите там , что “children это непрозрачная структура данных”. На самом деле они нам говорят о том, что props.children
может быть любым типом данных, например таким как массив, функция, объект и тп. В общем, это может быть всем чем угодно.
В React есть несколько вспомогательных функций, которые могут помочь управлять children
довольно легко и без лишних заморочек. Доступны они в React.Children
.
Проходимся циклом по children
Две самых очевидных из вышеупомянутых функций это React.Children.map
и React.Children.forEach
. Они работают так же как и их коллеги из методов массивов, за исключением того, что они также работают в момент передачи функции, объекта или чего-нибудь ещё непосредственно в children
.
class IgnoreFirstChild extends React.Component {
render() {
const children = this.props.children
return (
<div>
{React.Children.map(children, (child, i) => {
// игнорируем первого потомка
if (i < 1) return
return child
})}
</div>
)
}
}
<IgnoreFirstChild />
компонент из кода выше, проходится по всем своим потомкам, игнорируя первый и возвращая всех остальных.
<IgnoreFirstChild>
<h1>First</h1>
<h1>Second</h1> // <- Only this is rendered
</IgnoreFirstChild>
В нашем случае мы бы могли также использовать this.props.children.map
. Но чтобы произошло в случае, когда кто-нибудь бы передал функцию как потомка? this.props.children
был бы функцией, а не массивом и мы бы словили ошибку.
Но а с React.Children.map
это не будет вообще проблемой:
<IgnoreFirstChild>
{() => <h1>First</h1>} // <- Ignored 💪
</IgnoreFirstChild>
Считаем children
Так как this.props.children
может быть совершенно любым типом данных, проверка количества потомков у компонента может быть довольно сложным процессом. Наивно написав this.props.children.length
вы получите 12, если станете проверять строку "Hello World!"
, хоть там и будет один потомок.
Именно поэтому у нас и есть React.Children.count
:
class ChildrenCounter extends React.Component {
render() {
return <p>React.Children.count(this.props.children)</p>
}
}
Так мы получим количество потомков, вне зависимости от их типа данных:
// Renders "1"
<ChildrenCounter>
Second!
</ChildrenCounter>
// Renders "2"
<ChildrenCounter>
<p>First</p>
<ChildComponent />
</ChildrenCounter>
// Renders "3"
<ChildrenCounter>
{() => <h1>First!</h1>}
Second!
<p>Third!</p>
</ChildrenCounter>
Конвертируем children в массив
Ну и под конец, если ни один из методов выше вам не подходит, то вы можете конвертировать children
в массив при помощи метода React.Children.toArray
. Это было бы полезно в случае сортировки, например:
class Sort extends React.Component {
render() {
const children = React.Children.toArray(this.props.children)
return <p>{children.sort().join(' ')}</p>
}
}
<Sort>
// Тут мы используем контейнеры выражений, чтобы убедиться в том, что наши строки
// переданы как три отдельных потомка, а не как одна строка
{'bananas'}{'oranges'}{'apples'}
</Sort>
Пример выше рендерит строки, но уже отсортированные:
Обратите внимание, что возвращаемый React.Children.toArray
массив не содержит потомков с типом функция, а только React.Element
или строки.
Один единственный потомок
Если вы вспомните о предыдущем компоненте <Executioner />
, то он работает с одним потомком, который является функцией.
class Executioner extends React.Component {
render() {
return this.props.children()
}
}
Мы можем уточнить это условие с помощью propTypes
, что будет выглядеть примерно таким образом:
Executioner.propTypes = {
children: React.PropTypes.func.isRequired,
}
Тут выйдет сообщение в консоль, которое в принципе будет проигнорировано разработчиком. Вместо этого мы можем использовать React.Children.only
внутри рендера.
class Executioner extends React.Component {
render() {
return React.Children.only(this.props.children)()
}
}
В этом случае отрендерится только один единственный потомок в this.props.children
. Если их будет больше, чем один, то нам выкинет ошибку, следовательно, работа приложения будет приостановлена — идеально для тех случаев, когда ленивый разработчик пытается разобраться с вашим приложением.
Редактирование children
Мы можем рендерить произвольные компоненты как children
. Но продолжать контролировать их из родителя, а не из компонента из которого мы их рендерим. Давайте посмотрим на этот момент повнимательнее. Представим, что у нас есть компонент RadioGroup
, который может включать в себя несколько компонентов RadioButton
. (которые рендерят <input type="radio">
)
RadioButton’ы не рендерятся из RadioGroup
сами по себе, а используются как Children
. Что говорит о том, что где-то в нашем приложении у нас есть такой код:
render() {
return(
<RadioGroup>
<RadioButton value="first">First</RadioButton>
<RadioButton value="second">Second</RadioButton>
<RadioButton value="third">Third</RadioButton>
</RadioGroup>
)
}
В этом коде есть небольшая проблемка. Инпуты не сгруппированы, что приводит к такому результату:
Чтобы их сгруппировать им всем нужно иметь одинаковый атрибут name
. Мы бы конечно могли бы сразу назначить name
каждому RadioButton
:
<RadioGroup>
<RadioButton name="g1" value="first">First</RadioButton>
<RadioButton name="g1" value="second">Second</RadioButton>
<RadioButton name="g1" value="third">Third</RadioButton>
</RadioGroup>
Но это скучно и возможно приведёт к ошибкам. А ведь у нас в руках вся сила JavaScript! Можем ли мы использовать её, чтобы автоматически указать RadioGroup
нужное нам name
для всех его children
?
Меняем props у Children
Итак, в RadioGroup
мы добавим новый метод под названием renderChildren, который будет редактировать пропсы у children
:
class RadioGroup extends React.Component {
constructor() {
super()
this.renderChildren = this.renderChildren.bind(this)
}
renderChildren() {
return this.props.children
}
render() {
return (
<div className="group">
{this.renderChildren()}
</div>
)
}
}
Давайте пробежимся по всем потомкам, чтобы получить каждого из них по отдельности:
renderChildren() {
return React.Children.map(this.props.children, child => {
return child
})
}
Отлично, но как теперь мы можем редактировать их свойства?
Имутабельно клонируем элементы
И вот тут в игру вступает последний вспомогательный метод. Как и подразумевается под его названием, React.cloneElement
клонирует элемент. Мы передаём ему элемент, который желаем скопировать, как первый аргумент, а затем вторым аргументом передаём объект с пропсами, которые должны быть выставлены уже склонированному элементу:
const cloned = React.cloneElement(element, {
new: 'yes!'
})
Теперь элемент cloned
будет иметь пропс new
со значением "yes!"
.
И это именно то, что нам нужно для завершения RadioGroup
. Мы клонируем каждого потомка и выставляем ему пропс name
со значением this.props.name
.
renderChildren() {
return React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
name: this.props.name
})
})
}
Последним шагом мы передадим уникальный name
к RadioGroup
:
<RadioGroup name="g1">
<RadioButton value="first">First</RadioButton>
<RadioButton value="second">Second</RadioButton>
<RadioButton value="third">Third</RadioButton>
</RadioGroup>
Работает! Вместо указания вручную атрибутов для каждого RadioButton
, мы просто можем указать RadioGroup
то, что нам нужно, чтобы было name и дальше он позаботиться об этом.
Заключение
Children
заставляют компоненты в React вести себя как элементы разметки, а не отдельные объекты. Используя силу JavaScript и некоторые вспомогательные функции React, мы можем смело с ними работать для создания декларативных API и вообще для упрощения в целом для упрощения рабочего процесса.