前言
A higher-order component is a function that takes a component and returns a new component.
const EnhancedComponent = higherOrderComponent(WrappedComponent);複製程式碼
為何使用
- 程式碼複用:這是高階元件最基本的功能。元件是React中最小單元,兩個相似度很高的元件通過將元件重複部分抽取出來,再通過高階元件擴充套件,增刪改props,可達到元件可複用的目的;
- 條件渲染:控制元件的渲染邏輯,常見case:鑑權;
- 生命週期捕獲/劫持:藉助父元件子元件生命週期規則捕獲子元件的生命週期,常見case:打點。
如何使用
1、不要修改原始元件
3、保持可組合性
- props
- state
- ref
- 生命週期方法
- static方法
- React 元素樹
- 元件被掛載後,回撥函式被立即執行,回撥函式的引數為該元件的具體例項。
- 元件被解除安裝或者原有的ref屬性本身發生變化時,回撥也會被立即執行,此時回撥函式引數為null,以確保記憶體洩露。
- 原元件所在位置:如能否被包裹或包裹其他元件;
- 能否讀取到或操作原元件的props
- 能否讀取、操作(編輯、刪除)原元件的state
- 能否通過ref訪問到原元件中的dom元素
- 是否影響原元件某些生命週期等方法
- 是否取到原元件static方法
- 能否劫持原元件生命週期方法
- 能否渲染劫持
class Student extends React.Component {
static sayHello() {
console.log('hello from Student'); // eslint-disable-line
}
constructor(props) {
super(props);
console.log('Student constructor'); // eslint-disable-line
this.focus = this.focus.bind(this);
}
componentWillMount() {
console.log('Student componentWillMount'); // eslint-disable-line
this.setState({
name: this.props.name,
age: this.props.age,
});
}
componentDidMount() {
console.log('Student componentDidMount'); // eslint-disable-line
}
componentWillReceiveProps(nextProps) {
console.log('Student componentWillReceiveProps'); // eslint-disable-line
console.log(nextProps); // eslint-disable-line
}
focus() {
this.inputElement.focus();
}
render() {
return (<div style={outerStyle}>
<p>姓名:{this.state.name}</p>
<p>
年齡:
<input
style={inputStyle}
value={this.state.age}
ref={(input) => {
this.inputElement = input;
}}
/>
</p>
<p>
<input
style={buttonStyle}
type="button"
value="focus input"
onClick={this.focus}
/>
</p>
</div>);
}
}複製程式碼
1、直接返回一個stateless component,如:
function EnhanceWrapper(WrappedComponent) {
const newProps = {
source: 'app',
};
return props => <WrappedComponent {...props} {...newProps} />;
}複製程式碼
- √ 原元件所在位置(能否被包裹或包裹其他元件)
- √ 能否取到或操作原元件的props
- 乄 能否取到或操作state
- 乄 能否通過ref訪問到原元件中的dom元素
- X 是否影響原元件生命週期等方法
- √ 是否取到原元件static方法
- X 能否劫持原元件生命週期
- 乄 能否渲染劫持
關於ref的訪問,以上面的子元件Student為例,父元件:
import Student from '../components/common/Student';
function EnhanceWrapper(WrappedComponent) {
let inputElement = null;
function handleClick() {
inputElement.focus();
}
function wrappedComponentStaic() {
WrappedComponent.sayHello();
}
return props => (<div>
<WrappedComponent
inputRef={(el) => { inputElement = el; }}
{...props}
/>
<input
type="button"
value="focus子元件input"
onClick={handleClick}
/>
<input
type="button"
value="呼叫子元件static"
onClick={wrappedComponentStaic}
/>
</div>);
}
const WrapperComponent = EnhanceWrapper(ShopList);複製程式碼
<input
ref={(input) => {
this.inputElement = input;
}}
/>複製程式碼
<input
ref={(input) => {
this.inputElement = input;
this.props.inputRef(input);
}}
/>複製程式碼
function EnhanceWrapper(WrappedComponent) {
return class WrappedComponent extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}複製程式碼
- √ 原元件所在位置(能否被包裹或包裹其他元件)
- √ 能否取到或操作原元件的props
- 乄 能否取到或操作state
- 乄 能否通過ref訪問到原元件中的dom元素
- √ 是否影響原元件生命週期等方法
- √ 是否取到原元件static方法
- X 能否劫持原元件生命週期
- 乄 能否渲染劫持
function EnhanceWrapper(WrappedComponent) {
return class WrapperComponent extends React.Component {
static wrappedComponentStaic() {
WrappedComponent.sayHello();
}
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
console.log('WrapperComponent componentWillMount'); // eslint-disable-line
}
componentDidMount() {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
<WrappedComponent
inputRef={(el) => { this.inputElement = el; }}
{...this.props}
/>
<input
type="button"
value="focus子元件input"
onClick={this.handleClick}
/>
<input
type="button"
value="呼叫子元件static"
onClick={this.constructor.wrappedComponentStaic}
/>
</div>);
}
};
}複製程式碼
3、繼承(extends)原元件後返回一個新的class component,如:
function EnhanceWrapper(WrappedComponent) {
return class WrappedComponent extends WrappedComponent {
render() {
return super.render();
}
}
}複製程式碼
- √ 原元件所在位置(能否被包裹或包裹其他元件)
- √ 能否取到或操作原元件的props
- √ 能否取到或操作state
- √ 能否通過ref訪問到原元件中的dom元素
- √ 是否影響原元件生命週期等方法
- √ 是否取到原元件static方法
- √ 能否劫持原元件生命週期
- √ 能否渲染劫持
function EnhanceWrapper(WrappedComponent) {
return class WrapperComponent extends WrappedComponent {
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentDidMount(...argus) {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
if (didMount) {
didMount.apply(this, argus);
}
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
{super.render()}
<p>姓名:{this.state.name}</p>
<input
type="button"
value="focus子元件input"
onClick={this.handleClick}
/>
<input
type="button"
value="呼叫子元件static"
onClick={WrapperComponent.sayHello}
/>
</div>);
}
};
}複製程式碼
一些說明:
5:由於class繼承時會先生成父類的示例,所以 Student 的 constructor 會先於WrapperComponent 執行。其次,繼承會覆蓋父類的例項方法,所以在 WrapperComponent定義 componentDidMount 後Student的 componentDidMount 會被覆蓋不會執行。沒有被覆蓋的componentWillMount會被執行。
function EnhanceWrapper(WrappedComponent) {
const willMount = WrappedComponent.prototype.componentWillMount;
const didMount = WrappedComponent.prototype.componentDidMount;
return class WrapperComponent extends WrappedComponent {
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentWillMount(...argus) {
console.log('WrapperComponent componentWillMount'); // eslint-disable-line
if (willMount) {
willMount.apply(this, argus);
}
}
componentDidMount(...argus) {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
if (didMount) {
didMount.apply(this, argus);
}
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
{super.render()}
<p>姓名:{this.state.name}</p>
<input
type="button"
value="focus子元件input"
onClick={this.handleClick}
/>
<input
type="button"
value="呼叫子元件static"
onClick={WrapperComponent.sayHello}
/>
</div>);
}
};
}複製程式碼
場景舉例
場景1:頁面複用
import React from 'react';
class ShopList extends React.Component {
componentWillMount() {
}
render() {
// 使用this.props.data渲染
}
}
export default ShopList;複製程式碼
import ShopList from '../components/ShopList.jsx';
function shopListWithFetching(fetchData, defaultProps) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentWillMount() {
fetchData().then((list) => {
this.setState({
data: list,
});
}, (error) => {
console.log(error); // eslint-disable-line
});
}
render() {
return <ShopList data={this.state.data} {...defaultProps} {...this.props} />;
}
};
}
export default shopListWithFetching;複製程式碼
import React from 'react';
import ReactDOM from 'react-dom';
import getShopListA from '../lib/utils';
import shopListWithFetching from '../common/shopListWithFetching.jsx';
const defaultProps = {
emptyMsg: '暫無門店資料',
};
const SholistA = shopListWithFetching(getShopListA, defaultProps);
ReactDOM.render(<SholistA />, document.getElementById('app'));複製程式碼
import React from 'react';
import ReactDOM from 'react-dom';
import getShopListB from '../lib/utils';
import shopListWithFetching from '../components/ShopList.jsx';
const defaultProps = {
emptyMsg: '暫無合作的門店',
};
const SholistB = shopListWithFetching(getShopListB, defaultProps);
ReactDOM.render(<SholistB />, document.getElementById('app'));複製程式碼
場景2:頁面鑑權
- 幾個頁面:鑑權程式碼不能重複寫在頁面元件中;
- 只進行文案提示:鑑權過程在頁面部分生命週期(業務資料請求)之前;
- 一週後去掉白名單:鑑權應該完全與業務解耦,增加或去除鑑權應該最小化影響原有邏輯。
import React from 'react';
class Page1 extends React.Component {
componentWillMount() {
// 獲取業務資料
}
render() {
// 頁面渲染
}
}
export default Page1複製程式碼
import React from 'react';
class Page2 extends React.Component {
componentWillMount() {
// 獲取業務資料
}
render() {
// 頁面渲染
}
}
export default Page2
複製程式碼
import React from 'react';
import { whiteListAuth } from '../lib/utils';
/**
* 白名單許可權校驗
* @param WrappedComponent
* @returns {AuthWrappedComponent}
* @constructor
*/
function AuthWrapper(WrappedComponent) {
class AuthWrappedComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
permissionDenied: -1,
};
}
componentWillMount() {
whiteListAuth().then(() => {
// success
this.setState({
permissionDenied: 0,
});
}, (error) => {
this.setState({
permissionDenied: 1,
});
console.log(error);
});
}
render() {
if (this.state.permissionDenied === -1) {
return null;
}
if (this.state.permissionDenied) {
return <div>功能即將上線,敬請期待~</div>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthWrappedComponent;
}
export default AuthWrapper;複製程式碼
import React from 'react';
import AuthWrapper from '../components/AuthWrapper';
class Page1 extends React.Component {
componentWillMount() {
// 獲取業務資料
}
render() {
// 頁面渲染
}
}
// export default Page1
export default AuthWrapper(Page1);複製程式碼
import React from 'react';
import AuthWrapper from '../components/AuthWrapper';
class Page2 extends React.Component {
componentWillMount() {
// 獲取業務資料
}
render() {
// 頁面渲染
}
}
// export default Page2
export default AuthWrapper(Page2);
複製程式碼
場景3:日誌及效能打點
思路:通過extends方法返回高階元件,劫持原頁面元件的生命週期。具體可期待其他小夥伴後續的文章。
高階元件常見問題
常用高階元件庫
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)複製程式碼
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
return connectHOC(selectorFactory, {...})
}複製程式碼
export default function connectAdvanced() {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
render() {
// 返回
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
}
// Similar to Object.assign
return hoistStatics(Connect, WrappedComponent)
}複製程式碼
Recompose is a React utility belt for function components and higher-order components.
/* eslint-disable no-console */
import { Component } from 'react'
import createEagerFactory from './createEagerFactory'
import setDisplayName from './setDisplayName'
import wrapDisplayName from './wrapDisplayName'
import mapValues from './utils/mapValues'
const withHandlers = handlers => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
class WithHandlers extends Component {
cachedHandlers = {}
handlers = mapValues(
typeof handlers === 'function' ? handlers(this.props) : handlers,
(createHandler, handlerName) => (...args) => {
const cachedHandler = this.cachedHandlers[handlerName]
if (cachedHandler) {
return cachedHandler(...args)
}
const handler = createHandler(this.props)
this.cachedHandlers[handlerName] = handler
if (
process.env.NODE_ENV !== 'production' &&
typeof handler !== 'function'
) {
console.error(
// eslint-disable-line no-console
'withHandlers(): Expected a map of higher-order functions. ' +
'Refer to the docs for more info.'
)
}
return handler(...args)
}
)
componentWillReceiveProps() {
this.cachedHandlers = {}
}
render() {
return factory({
...this.props,
...this.handlers,
})
}
}
return WithHandlers
}
export default withHandlers複製程式碼
function createContainerComponent(
Component: React.ComponentType<any>,
spec: RelayContainerSpec,
): RelayContainerClass {
const ComponentClass = getReactComponent(Component);
class RelayContainer extends React.Component<$FlowFixMeProps,
{
queryData: {[propName: string]: mixed},
rawVariables: Variables,
relayProp: RelayProp,
},
> {
render(): React.Node {
if (ComponentClass) {
return (
<ComponentClass
{...this.props}
{...this.state.queryData}
ref={'component'} // eslint-disable-line react/no-string-refs
relay={this.state.relayProp}
/>
);
} else {
// Stateless functional.
const Fn = (Component: any);
return React.createElement(Fn, {
...this.props,
...this.state.queryData,
relay: this.state.relayProp,
});
}
}
}
return RelayContainer;
}複製程式碼
Function as Child Components
class StudentWithAge extends React.Component {
componentWillMount() {
this.setState({
name: '小紅',
age: 25,
});
}
render() {
return (
<div>
{this.props.children(this.state.name, this.state.age)}
</div>
);
}
}複製程式碼
<StudentWithAge>
{
(name, age) => {
let studentName = name;
if (age > 22) {
studentName = `大學畢業的${studentName}`;
}
return <Student name={studentName} />;
}
}
</StudentWithAge>複製程式碼
1、程式碼結構上少掉了一層(返回高階元件的)函式封裝。
2、除錯時元件結構更加清晰;
3、從元件複用角度來看,父元件和子元件之間通過children連線,兩個元件其實又完全可以單獨使用,內部耦合較小。當然單獨使用意義並不大,而且高階元件也可以通過組合兩個元件來做到。
2、(返回子元件)函式只能進行呼叫,無法劫持劫持原元件生命週期方法或取到static方法;
4、由於子元件的渲染控制完全通過在父元件render方法中呼叫(返回子元件)函式,無法通過shouldComponentUpdate來做效能優化。
關於Mixins
90% of the time you don't need mixins, in general prefer composition via high order components. For the 10% of the cases where mixins are best (e.g. PureRenderMixin and react-router's Lifecycle mixin), this library can be very useful.
- Mixins introduce implicit dependencies
- Mixins cause name clashes
- Mixins cause snowballing complexity
兩者生命週期上的差異
HOC的生命週期依賴於其實現,而mixin中除了render之外其他的生命週期方法都可以重複且會呼叫,但不可以設定相同的屬性或者包含相同名稱的普通方法。重複的生命週期呼叫方法的順序是:mixin方法首先會被呼叫(根據mixins中的順序從左到右的進行呼叫),然後再是元件的中方法被呼叫。