【译】深入理解React Children

原文地址: http://mxstbr.blog/2017/02/react-children-deepdive/

React的核心是组件。你可以像嵌套HTML tag一样嵌套React的组件,这使得JSX的语法十分简洁。

当我初次接触React的时候,我以为只要代码中会使用props.children就万事大吉了。伙计们,我错了。

因为当我们使用Javascript时,我们可以改变子组件。我们可以传递一些特殊的property, 决定是否render以及按照我们的想法控制它们。现在我们来深入的了解一下React子组件(React children)。

子组件

比如我们有一个<Grid/>组件,里面嵌套了<Row/>组件,我们可以这样写:

<Grid>
  <Row />
  <Row />
  <Row />
</Grid>

这三个Row组件被作为props.children传给了Grid。用括号表达式父组件即可渲染子组件。


class Grid extends React.Component {
  render() {
    return <div>{this.props.children}</div>
  }
}

父组件也可以决定是否渲染子组件, 以及在渲染前修改它们。比如<Fullstop />组件根本不渲染任何子组件:

class Fullstop extends React.Component {
  render() {
    return <h1>Hello world!</h1>
  }
}

不管你传什么子组件给Fullstop, 它永远只会显示Hello world!

注:这里<h1>组件还是渲染了它的children, 也就是Hello world!字符串。

任何东西都可以作为子组件

React Children也不要求一定是React组件,它可以是任何东西。比如上面的例子中,我们可以传一些文本作为children,一样没问题。

<Grid>Hello world!</Grid>

JSX会自动去除行首和行尾的空格,以及空白行,以及压缩字符串中间的空行为一个空格。

以下的例子渲染出来的结果是一样的:

<Grid>Hello world!</Grid>

<Grid>
  Hello world!
</Grid>

<Grid>
  Hello
  world!
</Grid>

<Grid>

  Hello world!
</Grid>

你也可以混合多种不同类型的chidren:

<Grid>
  Here is a row:
  <Row />
  Here is another row:
  <Row />
</Grid>

函数作为子组件

我们可以使用任何Javascript表达式作为子组件,这当然包括了函数。请看以下例子:

class Executioner extends React.Component {
  render() {
    // See how we're calling the child as a function?
    //                        ↓
    return this.props.children()
  }
}

这样使用这个组件:

<Executioner>
  {() => <h1>Hello World!</h1>}
</Executioner>

这个例子当然也不具有什么实际价值,但是可以说明这个用法。

设想你需要从服务器端拉取一些数据,你有很多种方法,函数子组件就是其一:

<Fetch url="api.myself.com">
  {(result) => <p>{result}</p>}
</Fetch>

修改子组件

如果你看过React官方文档,你会看到说"子组件是一种不透明的数据结构"(opaque data structure)。意思就是props.children可以是任何类型,比如array, function, object等等。因为什么都可以传,所以你也不能确定传过来了什么东西。

React提供了一些帮助函数来简化子组件的操作,它们位于React.Children下。

循环遍历子组件

最常用的帮助函数是React.Children.map和React.Children.forEach。它们和array对象的同名方法是等效的,并且当子组件是function, object时依然是有效的。

class IgnoreFirstChild extends React.Component {
  render() {
    const children = this.props.children
    return (
      <div>
        {React.Children.map(children, (child, i) => {
          // Ignore the first child
          if (i < 1) return
          return child
        })}
      </div>
    )
  }
}

<IgnoreFirstChild />组件忽略第一个子组件,然后返回所有其他的子组件。

<IgnoreFirstChild>
  <h1>First</h1>
  <h1>Second</h1> // <- Only this is rendered
</IgnoreFirstChild>

这个例子里,我们也可以使用this.props.children.map, 但是如果有人传了一个function进来,那么this.props.children是一个function, 我们就会得到错误:



但是如果使用React.Children.map就没有问题了:

<IgnoreFirstChild>
  {() => <h1>First</h1>} // <- Ignored 💪
</IgnoreFirstChild>

子组件个数计数

同样由于this.props.children类型的不确定,我们要判断有多少个子组件就比较困难了。如果幼稚的使用this.props.children.length就很容易报错了。而且,如果传来一个子组件"Hello World!",.length会返回12!

所以我们要使用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>

将子组件转化成Array

如果上面的方法都不是你需要的,那么你可以使用React.Children.toArray将子组件转化为Array。当你要对子组件排序时就特别有用了。

class Sort extends React.Component {
  render() {
    const children = React.Children.toArray(this.props.children)
    // Sort and render the children
    return <p>{children.sort().join(' ')}</p>
  }
}

<Sort>
  // We use expression containers to make sure our strings
  // are passed as three children, not as one string
  {'bananas'}{'oranges'}{'apples'}
</Sort>

上面的例子渲染排序后的水果:


强制传入单个子组件

再回到上面<Executioner />的例子。它期望传进来唯一的一个子组件,而且是个函数。

class Executioner extends React.Component {
  render() {
    return this.props.children()
  }
}

借助propTypes来实现:

Executioner.propTypes = {
  children: React.PropTypes.func.isRequired,
}

这样就会在console里打印出日志来,但是有的时候开发者很容易忽略这些消息。这个时候我们就应该在render方法里加入React.Children.only。

class Executioner extends React.Component {
  render() {
    return React.Children.only(this.props.children)()
  }
}

如果子组件多于一个会抛出一个错误,整个app会停止--绝对不会让一些偷懒的开发搞乱我们的组件。😎

修改子组件

我们可以渲染任意类型的子组件,但是我们可以从它们的父组件控制它们而不仅仅是从渲染它的组件去控制。什么意思呢,比如我们有一个RadioGroup组件,该组件包含一些RadioButton子组件(最终渲染成<input type="radio"> )。

RadioButtons不是由RadioGroup渲染的,它们作为子组件传入,这意味着我们的代码看起来像这样:

render() {
  return(
    <RadioGroup>
      <RadioButton value="first">First</RadioButton>
      <RadioButton value="second">Second</RadioButton>
      <RadioButton value="third">Third</RadioButton>
    </RadioGroup>
  )
}

这就有点问题了。Inputs没有在同一个Group里,所以出现了:



为了将各个Inputs放于同一个group里面需要给他们设置相同的name属性。当然可以用下面这个笨方法:

<RadioGroup>
  <RadioButton name="g1" value="first">First</RadioButton>
  <RadioButton name="g1" value="second">Second</RadioButton>
  <RadioButton name="g1" value="third">Third</RadioButton>
</RadioGroup>

但是为什么不利用一下Javascript的灵活能力呢?

改变子组件属性

我们给代码做一点修改:

class RadioGroup extends React.Component {
  constructor() {
    super()
    // Bind the method to the component context
    this.renderChildren = this.renderChildren.bind(this)
  }

  renderChildren() {
  return React.Children.map(this.props.children, child => {
    // TODO: Change the name prop to this.props.name
    return child
  })
}

  render() {
    return (
      <div className="group">
        {this.renderChildren()}
      </div>
    )
  }
}

问题是我们怎么修改property呢?

Immutably cloning elements

本文最后一个工具函数React.cloneElement。第一个参数是拷贝源的React element, 第二个参数是prop object,clone以后会把这个prop object设置成属性给拷贝结果。

const cloned = React.cloneElement(element, {
  new: 'yes!'
})

cloned元素将持有名为new的属性,属性值为"yes!"。

利用cloneElement函数可以完成上面的需求了:

renderChildren() {
  return React.Children.map(this.props.children, child => {
    return React.cloneElement(child, {
      name: this.props.name
    })
  })
}

这样一来每一个子组件都可以获得父组件的name属性了。所以最后一步,我们只需要在父组件上加上name属性即可。

<RadioGroup name="g1">
  <RadioButton value="first">First</RadioButton>
  <RadioButton value="second">Second</RadioButton>
  <RadioButton value="third">Third</RadioButton>
</RadioGroup>

成功了。在这里我们没有在每个子组件上设置name属性,而是告诉父组件RadioGroup属性名和属性值,然后由RadioGroup来解决问题。

结语

子组件使得React组件之间变得不那么互相隔离,借助Javascript强大的能力和React的工具函数,现在我们能够很轻松的构建声明式API了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 本文为翻译文章,原文链接 React的核心为组件。你可以像嵌套HTML标签一样嵌套使用这些组件,这使得编写JSX更...
    yorklin阅读 40,952评论 12 60
  • 本笔记基于React官方文档,当前React版本号为15.4.0。 1. 安装 1.1 尝试 开始之前可以先去co...
    Awey阅读 7,532评论 14 128
  • 以下内容是我在学习和研究React时,对React的特性、重点和注意事项的提取、精练和总结,可以做为React特性...
    科研者阅读 8,106评论 2 21
  • He bears the accidents of life with dignity and grace, ma...
    whoosomi阅读 190评论 0 0
  • 《猜猜我有多爱你》是一本儿童绘本,里面的主人公小兔子要大兔子好好听着,他有多爱大兔子。可是无论小兔子说有多爱,大兔...
    文文心儿阅读 343评论 2 10