Погружаемся в работу с 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 и вообще для упрощения в целом для упрощения рабочего процесса.