组件复用
高阶组件(HOC)
高阶组件是 React 中用于复用组件逻辑的一种高级技巧. HOC 自身不是 React API 的一部分, 它是基于 React 特性的一种设计模式.
高阶组件是参数为组件, 返回值为新组件的函数:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
典型的例子: Redux 的connect
实现高阶组件有两种方式:
- 属性代理(Props Proxy): HOC 对传给 WrappedComponent 的 props 进行操作
- 反向继承(Ingeritance Inversion): HOC 继承 WrappedComponent
在讨论实现之前, 有些设计上的约定来帮助我们写出合理的 HOC
约定和原则
- 不要改变原始组件, 使用组合
- 不将不相关的 props 传递给被包裹的组件:
- 最大化可组合型: 函数柯里化等
- 包装显示名称以便轻松调试: 一般可以用
with开头的函数名称来命名 - 不要再 render 方法中使用 hoc
- 务必复制静态方法
属性代理
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
HOC 在 render 方法中返回了一个WrappedComponent类型的React Element, 我们还传入例如 HOC 接收到的 props, 这就是名字 Props Proxy 的由来.
Props Proxy 可以做什么:
- 操作 props
- 通过 Refs 访问到组件实例
- 提取 state
- 用其他元素包裹 WrappedComponent
操作 Props
读取, 添加, 编辑, 删除传给 WrappedComponent的props, 当删除或者编辑重要的 props 时要小心, 要确保高阶组件的 props 不会破坏WrappedComponent(用命名空间).
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
newProps = {
user: currentLoggedInUser
};
render() {
return <WrappedComponent {...this.props} {...this.newProps} />;
}
};
}
通过 Refs 访问到组件实例
function refsHOC(WrappedComponent) {
return class RefsHOC extends React.Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.method();
}
render() {
const props = Object.assign({}, this.props, { ref: this.proc.bind(this) });
return <WrappedComponent {...props} />;
}
};
}
提取 state
你可以通过传入 props 和回调函数把 state 提取出来,类似于 smart component 与 dumb component。
提取 state 的例子:提取了 input 的 value 和 onChange 方法。这个简单的例子不是很常规,但足够说明问题。
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
constructor(props) {
super(props);
this.state = {
name: ''
};
this.onNameChange = this.onNameChange.bind(this);
}
onNameChange(event) {
this.setState({
name: event.target.value
});
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange
}
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
使用:
@ppHOC
class Example extends React.Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
这样, 这个 input 就会自动称为受控组件
用其他元素包裹 WrappedComponent
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{ display: 'block' }}>
<WrappedComponent {...this.props} />
</div>
);
}
};
}
反向继承
Inheritance Inversion (II) 的最简实现:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render();
}
};
}
在这里的代码中, HOC 类继承了 WrappedComponent, 之所以被称为 Ingeritance Inversion, 是因为 WrappedComponent 被 Enhancer 继承了, 而不是 WrappedComponent 继承了 Enhancer
反向继承允许 HOC 通过 this 访问到 WrappedComponent, 意味着它可以访问到 state, props, 组件生命周期方法和 render 方法.
通过这个可以创建新的生命周期方法, 为了不破坏 WrappedComponent, 请调用super.[lifecycleHook]
一致化处理(Reconciliation process)
React 在处理字符串类型的 React 元素, 函数类型的 React 元素是, 会进行一致化处理, 解析成一个完全有字符串类型 React 组件组成的树, 再转换为 DOM 元素, 这意味着 Inheritance Inversion 的高阶组件不一定会解析完整子树.
渲染劫持
之所以被称为渲染劫持, 是因为 HOC 控制了 WrappedComponent 的渲染输出, 可以使用它:
- 在由 render 输出的任何 React 元素中读取、添加、编辑、删除 props
- 读取和修改由 render 输出的 React 元素树
- 有条件地渲染元素树
- 把样式包裹进元素树(就像在 Props Proxy 中的那样)
例如: 条件渲染:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
};
}
修改由 render 方法输出的 React 组件树:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
const elementsTree = super.render();
let newProps = {};
if (elementsTree && elementsTree.type === 'input') {
newProps = { value: 'may the force be with you' };
}
const props = Object.assign({}, elementsTree.props, newProps);
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children);
return newElementsTree;
}
};
}
在这个例子中,如果 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”。
你可以在这里做各种各样的事,你可以遍历整个元素树,然后修改元素树中任何元素的 props。这也正是样式处理库 Radium 所用的方法(案例分析一节中有更多关于 Radium 的信息)。
例子:通过访问 WrappedComponent 的 props 和 state 来做调试。
export function IIHOCDEBUGGER(WrappedComponent) {
return class II extends WrappedComponent {
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p>
<pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
);
}
};
}
用 HOC 包裹一个组件会使其失去原本的组件名称, 可以这样:
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//或
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
渲染属性(Render Props)
具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。
<DataProvider render={data => <h1>Hello {data.target}</h1>} />
以复用一个鼠标事件为例:
class MouseTracker 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}>
<h1>移动鼠标!</h1>
<p>
当前的鼠标位置是 ({this.state.x}, {this.state.y})
</p>
</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 中的静态组合, render props 的组合是动态的, 每次组合都发生在 render 内部. 并且任何 HOC 都能使用 render props 替代.
const withMouse = Component => {
return class extends React.Component {
render() {
return <Mouse render={mouse => <Component {...this.props} mouse={mouse} />} />;
}
};
};
React Hook
React Hook 也能很好的进行组件的逻辑复用, 这里简单的举一个例子:
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
这个封装的逻辑, 可以的多个组件内重复的使用:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return <li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>;
}
并且这两个 state 是完全独立的, 因为 Hook 是一种状态逻辑的方式, 并不是对数据本身的复用
Mixin
最后提一下已经不建议使用 Mixin 方法:
import React from 'react';
import ReactDOM from 'react-dom';
// mixin 中含有了你需要在任何应用中追踪鼠标位置的样板代码。
// 我们可以将样板代码放入到一个 mixin 中,这样其他组件就能共享这些代码
const MouseMixin = {
getInitialState() {
return { x: 0, y: 0 };
},
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
};
const App = React.createClass({
// 使用 mixin!
mixins: [MouseMixin],
render() {
const { x, y } = this.state;
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>
The mouse position is ({x}, {y})
</h1>
</div>
);
}
});
ReactDOM.render(<App />, document.getElementById('app'));
mixin 不建议被使用一方面是因为官方使用 class 作为创建组件的方法后, ES6 class 不支持 mixins, 并且 mixin 改变了 state, 此外还可能存在名字冲突等问题.
小结
mixin
- 组件与 mixin 之间是隐式依赖: 导致难以快速理解组件行为, 组件自身的 state 不能轻易修改
- 多个 mixin 可能会冲突
- mixin 倾向于增加更多状态, 增加复杂度, 降低可预测性
HOC
- HOC 通过 props 影响内部组件状态, 降低了耦合度
- 具有天然的层级结构, 降低了复杂度
- 无法从外部访问子组件的 state, 因为无法通过 shouldComponentUpdate 过滤不必要的更新(PureComponent)
- Ref 被隔断(forwardRef)
- 外层包装组件是不可见的
Render Props
- 解决的 HOC 的一些问题
- 将组件嵌套转换为了函数回调的嵌套
React Hook
- 解决嵌套问题, 使用更加简洁
- 更方便的将逻辑与 UI 分离
- 可以使用 Hook 组合
- 复用成本低
- 额外的学习成本
- 破坏了 PureComponent,React.memo 的浅比较的性能优化效果(为了取最新的 props 和 state,每次 render()都要重新创建事件处函数)
- 在闭包场景可能会引用到旧的 state、props 值
- 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
- React.memo 并不能完全替代 shouldComponentUpdate(因为拿不到 state change,只针对 props change)