Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

Погружаемся в работу с children на React

В этой статье мы погрузимся в работу с children на React и узнаем как можно с ними работать на реальных примерах.

Перевод A deep dive into children in 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 и вообще для упрощения в целом для упрощения рабочего процесса.