【React进阶】浅析React.Children.map和cloneElement设计组件

2,189 阅读3分钟

前言:本文介绍了顶层API的使用,大多数情况,我们都是对市面上的封装的组件拿来即用,所以几乎用不到任何顶层API来写通用型的业务代码。但是往往都是这些顶层API的支持得以我们在用这些组件时候得心应手。所以,了解还是需要的,我这里大致拿它用法来做浅析。

1、React.Children.map

每个this.props.children 调用后面的function方法,并且这个api最终返回一个数组 语法:

React.Children.map(children, function[(thisArg)])

它容易让人与js的数组map方法产生误解,实际上它与数组的map完全不是一个东西。什么意思呢?我们写段代码就知道了,假设我们代码是这样(这种组件写法方式在antd中非常多:steps、tabs等等):

// 子组件
function Child(props) {
  return (
    <div>
      do something...
    </div>
  );
}
// 父组件
function Father(props) {
  return (
    <div>
      {props.children}
    </div>
  );
}
// 使用
function Test() {
  return <div>
    <Father>
      <Child>111</Child>
      <Child>2222</Child>
    </Father>
  </div>;
}

如果现在我们需要用React.Children.map来给每个子组件加个什么属性的话(需要和React.cloneElement配合,待会讲)就如下:

import React, { Component } from 'react';

// 子组件
function Child(props) {
  return (
    <div>
      do something...
    </div>
  );
}

// 父组件
function Father(props) {
//看这里,它是把children传给map,后面的函数会依次执行,第一参数就是它本身
  const reactNode = React.Children.map(props.children, function(item, index) {
    console.log('item', item);
    return item;
  });
  return (
    <div>
      {reactNode}
    </div>
  );
}

// 使用
function Test() {
  return <div>
    <Father>
      <Child>111</Child>
      <Child>2222</Child>
    </Father>
  </div>;
}

上面的item打印出来结果:打印了了两个item,因为有两个子节点。

image.png并且reactNode的值是为数组[item,item]

同等写法,可以用props.children.map 不同点是这个map用的数组的map,相当于我们直接拿子节点来遍历了。

const reactNode = props.children.map((item, index) => {
  return item;
});

而antd中使用的是toArray

import toArray from 'rc-util/lib/Children/toArray'; 

const reactNode = toArray(props.children).map((child, index) => {
   return child
})

实际上效果都是一样的。

2、React.Children.forEach

它的用法和React.Children.map相同,唯一区别是它没有返回值,相对用的少。

3、React.cloneElement(重点)

以 element 元素为样板克隆并返回新的 React 元素 语法:

React.cloneElement(
  element,
  [config],
  [...children]
)

它的使用效果同于:

const nreProps = {...props, test:1}
<div {...newProps}>{children}</div>

把props拷贝并且赋予新的props,如果熟悉高阶组件HOC的同学一定知道能一下明白。 我们开头列举的React.Children.map来结合它使用一下看看效果:

import React, { Component } from 'react';

// 子组件
function Child(props) {
  return (
    <div>
      {props.children}
    </div>
  );
}

// 父组件
function Father(props) {
  const reactNode = props.children.map((child, index) => {
    return React.cloneElement(child, {
      ...child,
      style: { color: props.color },
    });
  });
  return (
    <div>
      {reactNode}
    </div>
  );
}

// 使用
function Test() {
  return <div>
    <Father color='red'>
      <Child>111</Child>
      <Child>2222</Child>
    </Father>
  </div>;
}

image.png

在保持子组件不变的情况下 父组件很轻松的把样式红色传递给了子组件。它在antd的中作为组件设计使用可以说是一个很频繁的顶层API,以Tabs为例子我们来对比Tabs组件的使用:

差的组件设计❌:

  <Tabs>
    <TabPan active={true} onclick={this.onclick} key='1'> 111 </TabPan>
    <TabPan active={false} onclick={this.onclick} key='2'> 222</TabPan>
    <TabPan active={false} onclick={this.onclick} key='3'> 333</TabPan>
  </Tabs>

激活的key和点击事件写在每个子组件上,变得非常繁琐。如果用cloneElement来设计的话,就会非常好用。

好的组件设计(antd中)✔:

  <Tabs  onclick={this.onclick} activeKey='1'>
    <TabPan key='1'> 1111 </TabPan>
  </Tabs>

对比结果一目了然。我们可以用不到50行的代码来实现同等antd的组件调用方式:

import React, {Component} from 'react';

class Tabs extends Component {

  state = {
    activeKey: this.props.defaultActiveKey
  }

  onChange = (activeKey) => {   
    this.setState({
      activeKey
    })
    this.props.onChange(activeKey)
  }

  render() {
    return (
      <div>
        {this.props.children.map((child, index) => {
          return React.cloneElement(child, {  // 通过克隆可以修改子组件的属性值
            ...child,
            active: child.props.id === this.state.activeKey,
            key: child.props.id,
            activeKey: this.state.activeKey,
            onChange: () => this.onChange(child.props.id)
          })
        })}
      </div>
    );
  }
}

class TabPane extends Component {
  render() {
    return (
      <div>
        <h1 style={{color: this.props.active && 'red'}} onClick={this.props.onChange}>
          {this.props.tab}
        </h1>
        <div> {(this.props.activeKey === this.props.id) && this.props.children}</div>
      </div>
    );
  }
}

使用:

import React, {Component} from 'react';

class Index extends Component {
  callback = (key) => {
    console.log('key', key)
  }
  render() {
    return (
      <div>
        <Tabs defaultActiveKey={1} onChange={this.callback}>
          <TabPane id={1} tab='tab-1'>
            Content of Tab Pane 111
          </TabPane>
          <TabPane id={2} tab='tab-2'>
            Content of Tab Pane 2222
          </TabPane>
          <TabPane id={3} tab='tab-3'>
            Content of Tab Pane 333
          </TabPane>
        </Tabs>
      </div>
    );
  }
}

最终效果:

24769903-50df8689d46f79a7.gif

这就是cloneElement带来的优势。