High Order Component(包裝元件,後面簡稱HOC),是React開發中提高元件複用性的高階技巧。HOC並不是React的API,他是根據React的特性形成的一種開發模式。
HOC具體上就是一個接受元件作為引數並返回一個新的元件的方法
const EnhancedComponent = higherOrderComponent(WrappedComponent)
複製程式碼
在React的第三方生態中,有非常多的使用,比如Redux的connect
方法或者React-Router的withrouter
方法。
舉個例子
我們有兩個元件:
// CommentList
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
複製程式碼
// BlogPost
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
複製程式碼
他們雖然是兩個不同的元件,對DataSource的需求也不同,但是他們有很多的內容是相似的:
- 在元件渲染之後監聽DataSource
- 在監聽器裡面呼叫setState
- 在unmout的時候刪除監聽器
在大型的工程開發裡面,這種相似的程式碼會經常出現,那麼如果有辦法把這些相似程式碼提取並複用,對工程的可維護性和開發效率可以帶來明顯的提升。
使用HOC我們可以提供一個方法,並接受不了元件和一些元件間的區別配置作為引數,然後返回一個包裝過的元件作為結果。
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
複製程式碼
然後我們就可以通過簡單的呼叫該方法來包裝元件:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
複製程式碼
注意:在HOC中我們並沒有修改輸入的元件,也沒有通過繼承來擴充套件元件。HOC是通過組合的方式來達到擴充套件元件的目的,一個HOC應該是一個沒有副作用的方法。
在這個例子中我們把兩個元件相似的生命週期方法提取出來,並提供selectData作為引數讓輸入元件可以選擇自己想要的資料。因為withSubscription是個純粹的方法,所以以後如果有相似的元件,都可以通過該方法進行包裝,能夠節省非常多的重複程式碼。
不要修改原始元件,使用組合進行功能擴充套件
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
};
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
複製程式碼
通過以上方式我們也可以達到擴充套件元件的效果,但是會存在一些問題
- 如果InputComponent本身也有
componentWillReceiveProps
生命週期方法,那麼就會被覆蓋 - functional component不適用,因為他根本不存在生命週期方法
修改原始元件的方式缺乏抽象化,使用者必須知道這個方法是如何實現的來避免上面提到的問題。
如果通過組合的方式來做,我們就可以避免這些問題
function logProps(InputComponent) {
return class extends React.Component{
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
<InputComponent {...this.props} />
}
}
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
複製程式碼
慣例:無關的props傳入到原始元件
HOC元件會在原始元件的基礎上增加一些擴充套件功能使用的props,那麼這些props就不應該傳入到原始元件(當然有例外,比如HOC元件需要使用原始元件指定的props),一般來說我們會這樣處理props:
render() {
// Filter out extra props that are specific to this HOC and shouldn't be
// passed through
const { extraProp, ...passThroughProps } = this.props;
// Inject props into the wrapped component. These are usually state values or
// instance methods.
const injectedProp = someStateOrInstanceMethod;
// Pass props to wrapped component
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
複製程式碼
extraProp
是HOC元件中要用的props,不用的剩下的props我們都認為是原始元件需要使用的props,如果是兩者通用的props你可以單獨傳遞。
慣例:包裝元件的顯示名稱來方便除錯
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製程式碼
簡單來說就是通過手動指定displayName
來讓HOC元件能夠更方便得被react devtool觀察到
慣例:不要在render方法裡面呼叫HOC方法
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
複製程式碼
一來每次呼叫enhance
返回的都是一個新的class,react的diffing演算法是根據元件的特徵來判斷是否需要重新渲染的,如果兩次render的時候元件之間不是(===)完全相等的,那麼會直接重新渲染,而部署根據props傳入之後再進行diff,對效能損耗非常大。並且重新渲染會讓之前的元件的state和children全部丟失。
二來React的元件是通過props來改變其顯示的,完全沒有必要每次渲染動態產生一個元件,理論上需要在渲染時自定義的引數,都可以通過事先指定好props來實現可配置。
靜態方法必須被拷貝
有時候會在元件的class上面外掛一下幫助方法,如果按照上面的方法進行包裝,那麼包裝之後的class就沒有來這些靜態方法,這時候為了保持元件使用的一致性,一般我們會把這些靜態方法拷貝到包裝後的元件上。
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
複製程式碼
這個之適用於你已知輸入元件存在那些靜態方法的情況,如果需要可擴充套件性更高,那麼可以選擇使用第三方外掛hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
複製程式碼
ref
ref作為React中的特殊屬性--類似於key,並不屬於props,也就是說我們使用傳遞props的方式並不會把ref傳遞進去,那麼這時候如果我們在HOC元件上放一個ref,拿到的是包裝之後的元件而不是原始元件,這可能就會導致一些問題。
在React 16.3之後官方增加來一個React.forwardRef
方法來解決這個問題,具體可以參考這裡
我是Jocky,一個專注於React技巧和深度分析的前端工程師,React絕對是一個越深入學習,越能讓你覺得他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裡