介绍俩平常没使用的React API,近日踩雷了,遂借此篇提出来品品...
首先这俩货同属于React的顶层API,即我们import React from 'react';
后,可以通过React.xxx
的方式来调用。
再看官方文档对它们的划分:
图中的几个API都是对React
元素进行操作的,isValidElement
就不赘述了,用来校验入参是否是一个合法的React
元素,返回一个布尔值。
React.Children
我们都知道在props
对象中还有children
这个属性。它能够从某种程度上减少我们在一个组件内的嵌套层级,可能这样描述有点抽象,举个栗子:
// 比如我们有个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.Children
的Children
是大写。
React.Children.map
React.Children.map(children, function[(thisArg)])
这个类方法能够cover前文我提到的未知数据结构下的遍历问题,只需要简单修改:
React.Children.map(props.children, child => {})
可以看到这个API接收两个参数,第一个就是我们通常要处理的黑盒prop.children
,第二个入参回调,其实就是我们遍历的元素上下文,通过它,我们能够进行定制化的操作。
笔者结合源码得到当props.children
为null
和undefined
时,最终会原值返回,其余情景则是返回一个数组。
React.Children.forEach
跟React.Children.map
类似,都是迭代操作,只不过这个不会返回数组。undefined
和null
时的判断逻辑同上。
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
。原element
的key
和ref
都会保留。
看下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信息,并且不再修改以上组件结构(不额外在每个子组件上加onClick
的props
),实际展示类似下图:
此时,我们已经了解了前文中介绍的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可以从一定程度上减少代码的重复书写,使组件标签表达更加清晰。