核心
- 高階元件不用關心資料是如何渲染的,隻用關心邏輯即可。
- 被包裝的元件不用關心資料是怎麼來的,隻用負責渲染即可。
- 最後渲染的是高階元件傳回的元件。
高階元件(HOC)是React中用于複用元件邏輯的一種進階技巧,HOC是基于React的組合特性而形成的設計模式。
解讀
- 高階元件是一種函數,接收的參數是元件,傳回的也是元件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
複制代碼
使用HOC解決橫切關注點問題
元件是React中代碼複用的基本單元,但是有些模式并不适合傳統元件。
- 下面的元件訂閱外部資料源,用以渲染評論清單,元件名為CommentList
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// 假設 "DataSource" 是個全局範圍内的資料源變量
comments: DataSource.getComments()
};
}
componentDidMount() {
// 訂閱更改
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 清除訂閱
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 當資料源更新時,更新元件狀态
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添加一個偵聽器。
- 在偵聽器内部,當資料源發生變化變化時,調用setState。
- 在解除安裝時,删除偵聽器。
上面兩個元件的不同點
- DataSource上調用不同的方法。
上面兩個元件相同點的地方被不斷的重複調用,在大型項目中,是以我們需要将這些共同使用的地方給抽象出來,然後讓許多元件之間共享它,這正是高階元件擅長的地方。
- 編寫一個建立元件函數,這個函數接收兩個參數,一個是要被包裝的子元件,另一個則是該子元件訂閱資料的函數。
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
複制代碼
- 當渲染 CommentListWithSubscription 和 BlogPostWithSubscription 時, CommentList 和 BlogPost 将傳遞一個 data prop,其中包含從 DataSource 檢索到的最新資料.
// 此函數接收一個元件...
function withSubscription(WrappedComponent, selectData) {
// ...并傳回另一個元件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...負責訂閱相關的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新資料渲染被包裝的元件!
// 請注意,我們可能還會傳遞其他屬性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
複制代碼
HOC不會修改傳入的元件,也不會使用繼承來複制其行為,相反HOC是通過将元件包裝在容器元件中來組成新的元件,HOC是純函數,沒有副作用。
- 被包裝元件接收來自容器元件的所有prop,同時也接收一個新的用于render的data prop
- HOC不用關心資料的使用方式,被包裝元件也不用關心資料是怎麼來的。
不要改變原始元件,使用組合
- 不要試圖在HOC中修改元件原型
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// 傳回原始的 input 元件,暗示它已經被修改。
return InputComponent;
}
// 每次調用 logProps 時,增強元件都會有 log 輸出。
const EnhancedComponent = logProps(InputComponent);
複制代碼
- 上面這種寫法會造成另一個同樣會修改componentDidUpate的HOC增強它,那麼前面的HOC就會失效。
- HOC不應該修改傳入元件,而應該使用組合的方式,将元件包裝在容器元件中實作功能。
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 将 input 元件包裝在容器中,而不對其進行修改。Good!
return <WrappedComponent {...this.props} />;
}
}
}
複制代碼
約定:将不相關的props傳遞給被包裹的元件
HOC為元件添加特性,自身不應該大幅改變約定,HOC應該透傳與自身無關的props,大多數HOC都應該包含一個類似于下面的render方法
render() {
// 過濾掉非此 HOC 額外的 props,且不要進行透傳
const { extraProp, ...passThroughProps } = this.props;
// 将 props 注入到被包裝的元件中。
// 通常為 state 的值或者執行個體方法。
const injectedProp = someStateOrInstanceMethod;
// 将 props 傳遞給被包裝元件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
複制代碼
約定:最大化可組合性
- 有時候HOC僅僅接收一個參數,也就是被包裹的元件
const NavbarWithRouter = withRouter(Navbar);
複制代碼
- HOC通常可以接收多個參數
const CommentWithRelay = Relay.createContainer(Comment, config);
複制代碼
- React Redux的connect函數
// React Redux 的 `connect` 函數
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
複制代碼
- 讓我們将connect函數進行拆分
// connect 是一個函數,它的傳回值為另外一個函數。
const enhance = connect(commentListSelector, commentListActions);
// 傳回值為 HOC,它會傳回已經連接配接 Redux store 的元件
const ConnectedComment = enhance(CommentList);
複制代碼
總結:connect就是一個傳回高階元件的高階函數。這個函數可能看起來難懂,但是實際上這個函數傳回了一個高階元件,然後這個高階元件接收一個元件作為參數,最後再傳回一個元件。
// 而不是這樣...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... 你可以編寫組合工具函數
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
// 這些都是單參數的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複制代碼
約定:包裝顯示名稱以便輕松調試
HOC建立的容器元件會和任何其他元件一樣,顯示在React Developer Tools中,為了友善調試,需要選擇顯示一個名稱,以表明他是HOC的産物。
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複制代碼
使用高階元件的注意事項
- 不要在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;
}
複制代碼
- refs不會被傳遞
雖然高階元件的約定式将所有props傳遞給包裝元件,但是這對refs并不适用,因為ref實際上并不是一個prop,關于這個問題,我的專欄裡前面介紹ref的講過這個問題。