本期精读的文章是:我不再使用高阶组件。
懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。
React 与 Vue 相比,组件为一等公民是最大特色之一。
由于组件可以作为一个 props 向下传递,因此 React 具备了高度抽象化的能力,Vue 虽然更易上手,但因 template 特点,没有所谓 props 传递组件这种概念,但这样导致在抽象能力上落后于 React。可能这是 JSX 与 template 之间的差异吧,也是变量与字符串之间的差异,变量同名但含义不同,所以可抽象,而模版靠规则和名称确定含义。
当然 Vue 也有 babel-plugin-transform-vue-jsx 这里不做展开。
强大的组件能力,导致了实践的多样性,高阶组件就是其一。高阶组件的特点是,JSX 描述的子元素,会注入到父级组件的 this.props.children
中,因此可以无入侵增强组件能力,常用比如权限、跳转、埋点、异常、描述、注入等等。
高阶组件也带来了使用中的困扰,作者这篇文章阐述了高阶组件存在的问题,值得我们了解。
高阶组件由于可嵌套,如果有一环高阶组件没有将内部 wrappedComponent
暴露出来,会导致后续叠加的高阶组件都无法获取、注入到原始组件。
另外就算所有高阶组件都遵循了规范,组件也难以察觉被注入的数据是由哪些高阶组件提供的,而且高阶组件之间互相隔离,导致可能存在覆盖 props 的危险情况,这些问题高阶组件都束手无策。这体现出约定比约束更加效率,但约定的可维护性低于约束。
因此更好的解决思路可能是叫做 render props
render callback
function as child
这些名字的方法,组件定义如下:
// Contrived example for simplicity
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Caffeinate extends Component {
propTypes = { children: PropTypes.func.isRequired };
state = { coffee: "Americano" };
render() {
return this.props.children(this.state.coffee);
}
}
直接将函数作为子元素,可以认为是一个匿名组件:
render(
<Caffeinate>
{(beverage) => <div>Drinking an {beverage}.</div>}
</Caffeinate>,
document.querySelector("#root")
);
//=> Drinking an Americano.
这种用法在 React Motion React Router 里都有采用。
我们看另一种写法:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Caffeinate extends Component {
propTypes = { children: PropTypes.func.isRequired };
state = { coffee: "Americano" };
render() {
return this.props.child(this.state.coffee);
}
}
// usage
render(
<Caffeinate child={
(beverage) => <div>Drinking an {beverage}.</div>
} />,
document.querySelector("#root")
);
render props
本质上与上面这种很常规的写法没什么不同,差异在于利用了 props.children
,将参数写在 JSX 的子元素中。相比高阶组件用法,这样嵌套下来,看得清楚数据流动,解决了高阶组件反复嵌套导致的各类问题:
render(
<RenderProps1>
{(title) => <div>
<h1>{title}</h1>
<RenderProps2>
{(name) =>
<RenderProps3>
{(age) => {
<div>{name}, {age}</div>
}}
</RenderProps3>
}
</RenderProps2>
</div>}
</RenderProps1>
)
与 HOC 相比,render props 开放性提升明显,原本 HOC 所做的功能抽象可通过 render Props 获取,而 render 也可以访问到父级的一切:
this.props.children
不该作为函数调用。- 渲染粒度变大,表格等需要性能优化的场景不适合。
- renderProps 渲染的并不是 React 组件,无法为其单独使用
redux
,mobx
dob
等依赖收集粒度也放不下去。
renderProps
为了解耦,让控制权从上到下传递,而底层实现不需要了解上层实现,这是解决 JSX 修改组件模版问题的方法之一,作为优化点之一,可以考虑让传入的 props 自身作为一个组件:
const View = ({title}) => <div>{title}</div>
// ...
render() {
return (
<Component view={View} />
)
}
与其绕那么大一圈,还不如回归到最普通的 props 传参,这说明 renderProps
作为其中一种特例,可能观赏价值大于其实用价值。其控制放权的思想也是为了解决组件 dom 结构定制化的问题。
但是这也涉及到限度的问题,以下就是两种极端:
render() {
return this.props.children
}
render() {
return (
<div>
<Header />
<Sidebar>
<Toolbox>
<ul>
<li>..</li>
<li>{this.props.secondLi}</li>
<li>..</li>
</ul>
</Toolbox>
</Sidebar>
<Footer />
</div>
)
}
可以看出,写出这两种代码的目的,都为了从外部控制组件结构,以至于最大限度提高组件的复用能力。其实很难在不了解组件自身含义时,妄下一个通用的结论,说 “你只要这么写,就能保证任何组件都非常通用”。
比如 Card
组件可以将 title
extra
设定为 ReactNode,加上 children
,其实用性已经足够了:
render() {
return (
<Container>
<Title>
{this.props.title}
{this.props.extra}
</Title>
<Body>
{this.props.children}
</Body>
</Container>
)
}
再比如 Modal
也只需要对 Header
Footer
children
支持 ReactNode 就可以保证足够的通用性。
在业务场景,由于代码修改频率较高,复用性重要程度就没那么高。
作者也提到了,高阶组件在某些场景很有用,所以不会完全拒绝使用。
在不为组件做注入的场景下是高阶组件的好场景,利用其生命周期实现权限、埋点,在层级少的时候用作依赖注入也非常方便。
其实程序员在思考这些最佳实践时,与艺术家的思考方式很类似,况且这些最佳实践在不同场景、不同团队,不同项目下都有所侧重,所以不用逮着所谓最完美的实践把代码全部重构,以后也全部用一种风格写代码。就像陶瓷艺术家也不会说:我再也不做彩瓷了,因为白瓷这种颜色非常简约,在我心中是完美的,因此我宁愿一辈子只做白瓷。
这一期也想表达一个积极含义,精读周刊是不会 give up 的!
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。