【面试问到就是不会系列】React.Children与React.cloneElement杂谈

 介绍俩日常没使用的React API,近日踩雷了,遂借此篇提出来品品...javascript

  首先这俩货同属于React的顶层API,即咱们import React from 'react';后,能够经过React.xxx的方式来调用。html

  再看官方文档对它们的划分:java

  图中的几个API都是对React元素进行操做的,isValidElement就不赘述了,用来校验入参是不是一个合法的React元素,返回一个布尔值。node

React.Children

  咱们都知道在props对象中还有children这个属性。它可以从某种程度上减小咱们在一个组件内的嵌套层级,可能这样描述有点抽象,举个栗子:react

// 好比咱们有个Modal模态框组件
export default class Modal extends React.Component {
    //...
}

// 有不少场景须要在Modal框内展现子组件的东西,最多见的结构相似下面

<Modal>
    <Content /> </Modal>

// 的确能够在定义Modal的文件内import子组件,但咱们这是一个公共的组件,它仅是一个套套,因此一般会使用下面这种方案

render() {
    return (
        <div> {this.props.children} </div>
    )
}
复制代码

  这样来讲,咱们的父组件就和可能传入的children解耦了,各个模块都是独立的,各司其职。更多的关于props.children的语法阐释能够阅读官方文档数组

  看到这里,咱们也发现了一个问题,就是props.children对于咱们开发者来讲就是一个黑盒,咱们对它可能传入的数据结构是不可知的(表达式、布尔、render function等等),若是咱们没有对其进行操做,那其实没什么所谓。但只要咱们对其进行操做了,好比下意识觉得是个数组进行props.children.map这样的调用就要注意,非Array就直接报TypeError了。那怎么处理相似这样的情景呢?数据结构

  其实React.Children刚好就是为咱们提供处理props.children数据结构能力的API。注意这里React.ChildrenChildren是大写dom

React.Children.map

  React.Children.map(children, function[(thisArg)])这个类方法可以cover前文我提到的未知数据结构下的遍历问题,只须要简单修改:函数

React.Children.map(props.children, child => {})
复制代码

  能够看到这个API接收两个参数,第一个就是咱们一般要处理的黑盒prop.children,第二个入参回调,其实就是咱们遍历的元素上下文,经过它,咱们可以进行定制化的操做。this

  笔者结合源码获得当props.childrennullundefined时,最终会原值返回,其他情景则是返回一个数组。

React.Children.forEach

  跟React.Children.map相似,都是迭代操做,只不过这个不会返回数组。undefinednull时的判断逻辑同上。

React.Children.count

  返回其中内部元素数,其值与前面两个迭代方法的回调触发次数相等。

React.Children.only

  用于判断传入的children是否只有一个child。注意接收类型是React element。不能拿React.Children.map()返回的结果再去判断是几个child,由于此时你拿到的已然是一个Array类型。

React.Children.toArray

  这个API会将黑盒的props.children数据结构以扁平的Array结构暴露给咱们,以下面这样:

  经常使用在往下传props时,从新排序或过滤部分children的情景。

React.cloneElement

  有了上面的铺垫,这个API的引入就比较天然了,前文中咱们经过React.Children的类方法获得了访问本是黑盒的props.children的能力。React.cloneElement则是能让咱们在操做React element时,进行浅层的新props merge,传入的新children则会替换旧的children。原elementkeyref都会保留。

  看下API定义:

React.cloneElement(
  element,
  [props],
  [...children]
)
复制代码

  其实跟React.createElement的构造有点像:

React.createElement(
  type,
  [props],
  [...children]
)
复制代码

  毕竟是拷贝返回一个新的组合元素,React.cloneElement处理element时能够大体理解成<element.type {...element.props} {...props}>{children}</element.type>

  那这个API到底有啥用呢?举一个场景:

<Tabs active=''>
    <Tab id='a' title='a'>
        Content: {Math.random()}
    </Tab>
    <Tab id='b' title='b'>
        Content: {Math.random()}
    </Tab>
    <Tab id='c' title='c'>
        Content: {Math.random()}
    </Tab>
</Tabs>
复制代码

  我但愿点击对应Tab的时候,再显示Content信息,而且再也不修改以上组件结构(不额外在每一个子组件上加onClickprops),实际展现相似下图:

  此时,咱们已经了解了前文中介绍的API的能力,大体有两种解决方案,主体思路是一致的,区分在是否是每一个子组件都挂一个回调亦或在父组件上挂一个事件代理,去判断。

  这里我使用HOOKS的函数式写法:

// 事件代理
const Tabs = props => {
    const { children, ...rest } = props;
    const [active, setActive] = useState(rest.active);
    let handleClick = e => {
        if (e.target.nodeName === 'A') {
            setActive(e.target.id);
        }
    }
    return (
        <header> <nav className={styles.nav}> <ul onClick={handleClick}> { React.Children.map(children, child => React.cloneElement(child, {active: active})) } </ul> </nav> </header>
    )
}
复制代码
// 每个child 都绑定回调 经过cloneElement传props
const Tabs = props => {
    const { children, ...rest } = props;
    const [active, setActive] = useState(rest.active);
    let toggleActive = (e, id) => {
        e.preventDefault();
        setActive(id);
    }
    return (
        <header> <nav className={styles.nav}> <ul> { React.Children.map(children, child => React.cloneElement(child, {active: active, toggleActive: toggleActive})) } </ul> </nav> </header>
    )
}
复制代码

  主体思想都相似,就是把子组件须要的属性和回调函数经过cloneElement的方式merge进去。

  以上DEMO,可借此传送门移步。

小结

  React.Children提供了咱们直接访问黑盒props.children数据结构的能力;

  React.cloneElement接收一个React element并支持往其中浅层合并props,替换旧children;笔者看来该API能够从必定程度上减小代码的重复书写,使组件标签表达更加清晰。

相关文章
相关标签/搜索