High Order Component(包裝組件,後面簡稱HOC),是React開發中提升組件複用性的高級技巧。HOC並非React的API,他是根據React的特性造成的一種開發模式。html
HOC具體上就是一個接受組件做爲參數並返回一個新的組件的方法前端
const EnhancedComponent = higherOrderComponent(WrappedComponent)
複製代碼
在React的第三方生態中,有很是多的使用,好比Redux的connect
方法或者React-Router的withrouter
方法。react
咱們有兩個組件:git
// 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的需求也不一樣,可是他們有不少的內容是類似的:github
在大型的工程開發裏面,這種類似的代碼會常常出現,那麼若是有辦法把這些類似代碼提取並複用,對工程的可維護性和開發效率能夠帶來明顯的提高。算法
使用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} />; } }; } 複製代碼
而後咱們就能夠經過簡單的調用該方法來包裝組件:app
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
複製代碼
注意:在HOC中咱們並無修改輸入的組件,也沒有經過繼承來擴展組件。HOC是經過組合的方式來達到擴展組件的目的,一個HOC應該是一個沒有反作用的方法。框架
在這個例子中咱們把兩個組件類似的生命週期方法提取出來,並提供selectData做爲參數讓輸入組件能夠選擇本身想要的數據。由於withSubscription是個純粹的方法,因此之後若是有類似的組件,均可以經過該方法進行包裝,可以節省很是多的重複代碼。post
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);
複製代碼
經過以上方式咱們也能夠達到擴展組件的效果,可是會存在一些問題
componentWillReceiveProps
生命週期方法,那麼就會被覆蓋修改原始組件的方式缺少抽象化,使用者必須知道這個方法是如何實現的來避免上面提到的問題。
若是經過組合的方式來作,咱們就能夠避免這些問題
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);
複製代碼
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() {
// 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做爲React中的特殊屬性--相似於key,並不屬於props,也就是說咱們使用傳遞props的方式並不會把ref傳遞進去,那麼這時候若是咱們在HOC組件上放一個ref,拿到的是包裝以後的組件而不是原始組件,這可能就會致使一些問題。
在React 16.3以後官方增長來一個React.forwardRef
方法來解決這個問題,具體能夠參考這裏
我是Jocky,一個專一於React技巧和深度分析的前端工程師,React絕對是一個越深刻學習,越能讓你以爲他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裏