読者です 読者をやめる 読者になる 読者になる

無駄と文化

実用的ブログ

React で this.props.children に新しい Props を渡す

f:id:todays_mitsui:20160814183151p:plain

React でカスタムコンポーネントを作るとき、コンポーネントの子要素には this.props.children でアクセスできます。
この this.props.children はそのままレンダリングすることもできるのですが、何かしらの Props を渡したくなったらどうするのでしょうか。

ざっくり調べた感じ Stack Overflow とか海外のブログにしか情報が無いようだったのでまとめてみます。


TL;DR

いきなり結論から、
this.props.children に直接 Props を渡すことはできません。
代わりの方法として、React.cloneElement() で React要素をクローンする時に Props を渡すことができるので、this.props.children をクローンしつつ Props を渡せばいいようです。

デモを用意しました。


以下のようにすればローカルでデモをいじりつつ試せます。

$ git clone https://github.com/todays-mitsui/passing-props-to-children-sample.git
$ cd passing-props-to-children-sample
$ npm install
$ npm start


要素に Props を渡す

this.props.children に限らず React要素は React.cloneElement() という API でクローンできます。
そのとき第2引数にオブジェクトを渡すと、React要素が持っている既存の Props とマージされた後、新しい Props として設定されるとのことです。

let elementWithProps = React.cloneElement(element, { foo: 'bar' })

公式ドキュメントに解説があるので、詳しくはそちらをどうぞ。


this.props.children に応じた処理をする

ところで this.props.children は状況によって様々な型の値になります。
複数の子要素を持っている場合には this.props.childrenReact要素の配列 になります。
その他、子要素がただのテキストノードだった場合には string に、子要素を持たない場合は undefined と、まぁ場合によっていろいろみたいです。

そんな this.props.children を上手く扱うために React.Children というユーティリティクラスが用意されています。
使うときには React.Children で参照するほかに、ES2015 で記述しているなら、

import { Children } from 'react'

というように個別にインポートしてもいいでしょう。


今回は React.Children.map() を使って子要素一つひとつに Props を渡します。

const newProps = { foo: 'bar' }

const childrenWithProps = React.Children.map(
  this.props.children,
  (child) => {
    // 各子要素をクローンしつつ newProps を渡す
    return React.cloneElement(child, newProps)
  }
)

さて、実はこれだけではいけません。
子要素がテキストノードを含んでいる場合には、上記の child に string が渡されます。そして React.cloneElement() は string を受け取ってくれないので、そこでエラーが発生します。

なので child の type を判別しつ上手いこと処理を分岐しましょう。
string が渡ってきた場合には何もせず、そのまま返すようにします。

const newProps = { foo: 'bar' }

const childrenWithProps = React.Children.map(
  this.props.children,
  (child) => {
    console.info(typeof child, child)

    switch (typeof child) {
      case 'string':
        // 子要素がテキストノードだった場合はそのまま return
        return child

      case 'object':
        // React要素だった場合は newProps を渡す
        return React.cloneElement(child, newProps)

      default:
        // それ以外の場合はとりあえず null 返しとく
        return null
    }
  }
)

これで this.props.children を複製しつつ Props を渡した childrenWithProps を作ることができました。


React.Children.map() についても公式ドキュメントの解説が親切でした。


簡単なデモ

やり方の説明としては以上なんですが、上記のコードは単体では動かないので実際に動作する簡単なデモを書きました。

ロジック部分は1ファイルでこのようになっています。

/* main.js */

import React, { Children } from 'react'
import ReactDom from 'react-dom'


class Child extends React.Component {
  render() {
    const parentName = this.props.parentName || 'UnknownParent'
    const name = this.props.name || 'UnknownChild'

    return (
      <p>
        ParentName: <em>{parentName}</em> > ChildName: <em>{name}</em>
      </p>
    )
  }
}

class Parent extends React.Component {
  render() {
    const newProps = { parentName: 'foo' }

    const childrenWithProps = Children.map(
      this.props.children,
      (child) => {
        console.info(typeof child, child)

        switch (typeof child) {
          case 'string':
            return child

          case 'object':
            return React.cloneElement(child, newProps)

          default:
            return null
        }
      }
    )

    return (
      <div>
        {childrenWithProps}
      </div>
    )
  }
}


ReactDom.render(
  (
    <Parent>
      ### Text Node ###
      <Child name='hoge' />
      <Child name='fuga' />
      <Child name='piyo' />
    </Parent>
  ),
  document.querySelector('.container')
)

<Child> コンポーネントは name, parentName という2つの Props を <p> タグの中で表示するだけの簡単なものです。
今回は parentName の方を親要素の <Parent> コンポーネントから渡してあげています。


今回のデモのソースは全て GitHub に置いてあります。


まとめ

React はもともと API が少なくて学習コストが少ないと思っているんですが、少ないなりに覚えておくと便利な API もありますね。
React.cloneElement()this.props.children をカスタマイズする以外にもいろいろな用途で使えるはずです。


私からは以上です。