前者属于 装饰器, 后者属于 控制反转。其中, hoc 是一个接受组件作为参数,而返回一个新组件的纯函数。render props 通过函数回调的方式提供一个接口,用以渲染外部组件。
HoC
组件是可复用的, 组件中的逻辑(属性、事件监听、函数处理及渲染函数)可以抽离出来。对于一般的逻辑,最简单的就是 utils 工具。对于包含状态的逻辑,需要 hoc。通过 hoc 解决的是横切关注点的问题。
render props
组件的复用,通过 props 传递值或回调。如果 props 是一个函数并且返回值是一个组件,该外部组件将通过此组件间接渲染。这种操作是不改变 v-dom 树的。
上层组件提供接口,下层组件实现接口。通过 props 提供接口,通过回调函数去提供实现。
规则
HoC
需求:只有登录时才渲染组件
const withLogin = (Component) => {
const NewComponent = (props) => {
if (getUserId()) {
return <Component {...props} />;
} else {
return null;
}
}
return NewComponent;
};
要点一、链式调用
// 高阶组件里使用生命周期函数
const withSection = Com => {
class GetSection extends Component {
state = {
section: ''
}
componentDidMount() {
// ajax
const section = '章节 - 高阶组件的class组件'
this.setState({ section })
}
render() {
return <Com {...this.props} section={this.state.section} />
}
}
return GetSection
}
const withUserLog = Com => {
class GetUserLog extends Component {
componentDidMount() {
console.log('记录了用户的阅读时间操作')
}
render() {
return <Com {...this.props} />
}
}
return GetUserLog
}
// 链式调用
export default compose(withUserLog, withSection)(Book)
// <=> 等价于
export default withUserLog(withSection(Book))
要点二、函数柯里化
import React from 'react'
import PropTypes from 'prop-types'
const isEmpty = (prop) => (
prop === null ||
prop === undefined ||
(prop.hasOwnProperty('length') && prop.length === 0) ||
(prop.constructor === Object && Object.keys(prop).length === 0)
)
export default (loadingProp) => (WrappedComponent) => {
const hocComponent = ({ ...props }) => {
return isEmpty(props[loadingProp]) ? <div className="loader" /> : <WrappedComponent {...props} />
}
hocComponent.propTypes = {
}
return hocComponent
}
要点三、传递不相关的 props
export default (loadingProp) => (WrappedComponent) => {
return class extends Component {
componentDidMount() {
this.startTimer = Date.now();
}
componentDidUpdate(prevProps, prevState) {
if(!isEmpty(nextProps[loadingProp])) {
this.endTimer = Date.now();
}
}
render() {
const myProps = {
loaddingTime: ((this.endTimer - this.startTimer)/1000).toFixed(2),
}
return isEmpty(this.props[loadingProp]) ? <div className="loader" /> :
<WrappedComponent {...this.props} {...myProps} />;
}
}
}
要点四、包装显示名称,以便轻松调试
高阶组件不得不处理 displayName,不然 debug 会很痛苦。当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类,而高阶组件总是创造一个新的 React 组件类,所以,每个高阶组件都需要处理一下 displayName。
获取视图名的通用方法
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default (loadingProp) => (WrappedComponent) => {
class WithLoadingHoc extends Component {
componentDidMount() {
this.startTimer = Date.now();
}
componentDidUpdate(prevProps, prevState) {
if(!isEmpty(nextProps[loadingProp])) {
this.endTimer = Date.now();
}
}
render() {
const myProps = {
loaddingTime: ((this.endTimer - this.startTimer)/1000).toFixed(2),
}
return isEmpty(this.props[loadingProp]) ? <div className="loader" /> :
<WrappedComponent {...this.props} {...myProps} />;
}
}
WithLoadingHoc.displayName = `WithLoadingHoc(${getDisplayName(WrappedComponent)})`;
return WithLoadingHoc;
}
render props
传统模式:低层组件直接调用高层组件(高 依赖与 低)
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class MouseWithCat extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
我们可以在这里换掉 <p> 的 <Cat> ......
但是接着我们需要创建一个单独的 <MouseWithSomethingElse>
每次我们需要使用它时,<MouseWithCat> 是不是真的可以重复使用.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<MouseWithCat />
</div>
);
}
}
控制反转模式: 高层提供接口,低层实现接口。高层不依赖于低层
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
结合 HoC,简化调用
// 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
// 使用具有 render prop 的普通组件创建一个!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
限制
HoC
要点一、复制静态方法
hoc 会将原始组件将使用容器组件包装成新组件,静态方法通过原组件暴露,而新组件则缺少这些静态方法。因此需要复制:
手动拷贝,你需要知道哪些方法应该被拷贝
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
要点二、务必转发 ref
这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。
import FancyButton from './FancyButton';
const ref = React.createRef();
// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
使用转发,向下传递
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogPros 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
要点三、不要在 render 方法中使用HoC
因为 hoc 的作用是返回一个新的组件,如果直接在 render 中调用 hoc 函数,每次 render 都会生成新的组件。对于复用的目的来说,这毫无帮助,之前此外生成的旧组件因此被不断卸载。
react 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 react 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树
如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。
render props
性能优化
箭头函数影响性能
class Mouse extends React.PureComponent {
// 与上面相同的代码......
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
{/*
这是不好的!
每个渲染的 `render` prop的值将会是不同的。
*/}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
不直接使用箭头函数
class MouseTracker extends React.Component {
// 定义为实例方法,`this.renderTheCat`始终
// 当我们在渲染中使用它时,它指的是相同的函数
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}