高阶组件
React 高阶组件详解
高阶组件(HOC)是
hocFactory:: W: React.Component => E: React.Component
其中
const EnhancedComponent = higherOrderComponent(WrappedComponent);
const higherOrderComponent = BaseComponent => {
// ...
// create new component from old one and update
// ...
return EnhancedComponent;
};
Props Proxy
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
在
操作props
我们可以读取、添加、编辑、删除传给
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
在
import { Subtract } from "utility-types";
export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
interface MakeCounterProps {
minValue?: number;
maxValue?: number;
}
interface MakeCounterState {
value: number;
}
const makeCounter = <P extends InjectedCounterProps>(
Component: React.ComponentType<P>
) =>
class MakeCounter extends React.Component<
Subtract<P, InjectedCounterProps> & MakeCounterProps,
MakeCounterState
> {
state: MakeCounterState = {
value: 0
};
increment = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.maxValue
? prevState.value
: prevState.value + 1
}));
};
decrement = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.minValue
? prevState.value
: prevState.value - 1
}));
};
render() {
const { minValue, maxValue, ...props } = this.props;
return (
<Component
{...(props as P)}
value={this.state.value}
onIncrement={this.increment}
onDecrement={this.decrement}
/>
);
}
};
通过Refs 访问到组件实例
你可以通过引用(ref)访问到
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
我们可以通过传入
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} />;
}
}
用其他元素包裹WrappedComponent
为了封装样式、布局或别的目的,你可以用其它组件和元素包裹
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{ display: "block" }}>
<WrappedComponent {...this.props} />
</div>
);
}
};
}
HOC 和参数
有时候我们也需要在
function HOCFactoryFactory(...params) {
// do something with params
return function HOCFactory(WrappedComponent) {
return class HOC extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
};
}
你可以这样用:
HOCFactoryFactory(params)(WrappedComponent);
//或
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component {}
案例分析
为此,我们首先定义一个名为{... this.props}
解构的优势,
import React from "react";
const withColor = BaseComponent => {
class EnhancedComponent extends React.Component {
getRandomColor() {
var letters = "0123456789ABCDEF";
var color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
render() {
return <BaseComponent color={this.getRandomColor()} {...this.props} />;
}
}
return EnhancedComponent;
};
export default withColor;
对于该
import React from "react";
import withColor from "./withColor";
const ColoredComponent = props => {
return <div style={{ background: props.color }}>{props.color}</div>;
};
export default withColor(ColoredComponent);
最后,可以在外部的应用中直接传入
import React, { Component } from "react";
import ColoredComponent from "./ColoredComponent";
class App extends Component {
render() {
return (
<div>
<ColoredComponent someProp="Prop 1" />
<ColoredComponent someProp="Prop 2" />
<ColoredComponent someProp="Prop 3" />
</div>
);
}
}
export default App;
Inheritance Inversion
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render();
}
};
}
可以看到,返回的
渲染劫持(Render Highjacking)
之所以被称为渲染劫持是因为
- 在由
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;
}
}
};
}
再如,修改由
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;
}
};
}
在这个例子中,如果
操作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.displayName = `HOC(${getDisplayName(WrappedComponent)})`;
//或
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
// ...
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
不要改变原始组件
不要试图在
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log("Current props: ", this.props);
console.log("Next props: ", nextProps);
};
// 返回原始的 input 组件,暗示它已经被修改。
return InputComponent;
}
// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);
这样做会产生一些不良后果。其一是输入组件再也无法像
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log("Current props: ", this.props);
console.log("Next props: ", nextProps);
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改。Good!
return <WrappedComponent {...this.props} />;
}
};
}
该
不要在render 方法中使用HOC
render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return <EnhancedComponent />;
}
这不仅仅是性能问题
务必复制静态方法
有时在
// 定义静态函数
WrappedComponent.staticMethod = function() {
/*...*/
};
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined"; // true
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
function enhance(WrappedComponent) {
class Enhance extends React.Component {
/*...*/
}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
但要这样做,你需要知道哪些方法应该被拷贝。你可以使用
import hoistNonReactStatic from "hoist-non-react-statics";
function enhance(WrappedComponent) {
class Enhance extends React.Component {
/*...*/
}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
除了导出组件,另一个可行的方案是再额外导出这个静态方法。
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...单独导出该方法...
export { someFunction };
// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from "./MyComponent.js";
案例分析
通用数据加载
在以下示例中,我们将开发一个高阶组件,该组件接受
首先,我们将首先创建一个高阶组件函数。我们将使用
import React from "react";
const withLoader = (BaseComponent, apiUrl) => {
class EnhancedComponent extends React.Component {
state = {
data: null
};
componentDidMount() {
fetch(apiUrl)
.then(res => res.json())
.then(data => {
this.setState({ data });
});
}
render() {
if (!this.state.data) {
return <div>Loading ...</div>;
}
return <BaseComponent data={this.state.data} {...this.props} />;
}
}
return EnhancedComponent;
};
export default withLoader;
使用此
import React from "react";
import withLoader from "./withLoader";
const Users = props => {
return (
<div>
<h1>Users:</h1>
<ul>
{props.data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default withLoader(Users, "https://jsonplaceholder.typicode.com/users");
类似地,我们也可以有如下的加载
import React from "react";
import withLoader from "./withLoader";
const Posts = props => {
return (
<div>
<h1>Posts:</h1>
<ul>
{props.data.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
);
};
export default withLoader(Posts, "https://jsonplaceholder.typicode.com/posts/");
Props Throttle
代码定义如下:
import { createElement, Component } from "react";
import getDisplayName from "react-display-name";
import throttle from "just-throttle";
const throttleHandler = (handlerName, interval, leadingCall) => Target => {
class ThrottleHandler extends Component {
constructor(props, context) {
super(props, context);
const intervalValue =
typeof interval === "function" ? interval(props) : interval;
this.throttlePropInvoke = throttle(
(...args) => this.props[handlerName](...args),
intervalValue,
leadingCall
);
this.handler = (e, ...rest) => {
if (e && typeof e.persist === "function") {
e.persist();
}
return this.throttlePropInvoke(e, ...rest);
};
}
render() {
return createElement(Target, {
...this.props,
[handlerName]: this.handler
});
}
}
if (process.env.NODE_ENV !== "production") {
ThrottleHandler.displayName = `throttleHandler(${getDisplayName(Target)})`;
}
return ThrottleHandler;
};
export default throttleHandler;
使用案例如下:
const Demo = ({ count, onButtonClick, label }) => (
<div className="demo">
{label || ""}
<h1>{count}</h1>
<button onClick={onButtonClick}>CLICK ME FAST</button>
</div>
);
const Demo1 = compose(
withState("count", "setCount", 0),
withHandlers({
onButtonClick: ({ count, setCount }) => () => setCount(count + 1)
}),
throttleHandler("onButtonClick", 1000)
)(Demo);
const Demo2 = compose(
withState("count", "setCount", 0),
withHandlers({
onButtonClick: ({ count, setCount }) => () => setCount(count + 1)
}),
throttleHandler("onButtonClick", props => props.throttle || 0)
)(Demo);
const MainDemo = () => (
<div>
<style>
{`.demo {
margin-bottom: 10px;
border-style: dotted;
border-radius: 10px;
padding: 5px;
}`}
</style>
<Demo1 label="Delay as argument" />
<Demo2 label="Delay from props" throttle={300} />
<Demo2 label="No delay (zero by default)" />
</div>
);
export default MainDemo;