?
This document uses PHP Chinese website manual Release
高階組件(HOC)是React中用于重用組件邏輯的高級技術(shù)。HOC本身不是React API的一部分。它們是從React的構(gòu)圖本質(zhì)中浮現(xiàn)出來的一種模式。
具體而言,高階組件是一個接收組件并返回新組件的函數(shù)。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
盡管組件將道具轉(zhuǎn)換為UI,但高階組件會將組件轉(zhuǎn)換為另一個組件。
HOC在第三方React庫中很常見,例如Redux connect
和Relay createContainer
。
在本文中,我們將討論為什么高階組件有用,以及如何編寫自己的。
注意 我們以前推薦mixin作為處理交叉問題的一種方式。之后我們意識到mixin會造成比他們的價值更大的麻煩。了解更多關(guān)于我們?yōu)槭裁措x開mixin以及如何轉(zhuǎn)換現(xiàn)有組件的更多信息。
組件是React中代碼重用的主要單元。但是,您會發(fā)現(xiàn)某些模式不適合傳統(tǒng)組件。
例如,假設(shè)您有一個CommentList
組件訂閱外部數(shù)據(jù)源來呈現(xiàn)評論列表:
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> ); }}
之后,您將編寫一個組件訂閱單個博客帖子,該帖子遵循類似的模式:
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} />; }}
CommentList
并且BlogPost
不完全相同 - 它們調(diào)用不同的方法DataSource
,并且它們呈現(xiàn)不同的輸出。但是他們的大部分實現(xiàn)都是一樣的:
在mount上,添加一個更改監(jiān)聽器DataSource
。
在監(jiān)聽器內(nèi)部,setState
每當數(shù)據(jù)源發(fā)生變化時都會調(diào)用。
在卸載時,刪除更改偵聽器。
你可以想象,在一個大型應(yīng)用程序中,訂閱DataSource
和調(diào)用的相同模式setState
將會一遍又一遍地發(fā)生。我們需要一種抽象,使我們能夠在單個地方定義這種邏輯,并在多個組件之間共享這些邏輯。這是高階元件擅長的地方。
我們可以編寫一個創(chuàng)建組件的函數(shù),比如CommentList
和BlogPost
訂閱DataSource
。該函數(shù)將接受作為其參數(shù)之一的接收訂閱數(shù)據(jù)作為道具的子組件。我們來調(diào)用這個函數(shù)withSubscription
:
const CommentListWithSubscription = withSubscription( CommentList, (DataSource) => DataSource.getComments());const BlogPostWithSubscription = withSubscription( BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id));
第一個參數(shù)是包裝組件。第二個參數(shù)檢索我們感興趣的數(shù)據(jù),給出一個DataSource
和當前的道具。
當CommentListWithSubscription
與BlogPostWithSubscription
被渲染,CommentList
并且BlogPost
將傳遞一個data
與從檢索到的最新的數(shù)據(jù)道具DataSource
:
// This function takes a component... 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} />; } };}
請注意,HOC不會修改輸入組件,也不會使用繼承來復(fù)制其行為。相反,HOC 通過將其包裝在容器組件中來組成原始組件。HOC是一種純粹的功能,具有零副作用。
就是這樣!被包裝的組件接收容器的所有道具以及一個新的道具,data
它用來渲染其輸出。HOC不關(guān)心如何或為什么使用數(shù)據(jù),并且封裝的組件不關(guān)心數(shù)據(jù)來自何處。
因為withSubscription
是一個正常的函數(shù),所以你可以添加盡可能多或者很少的參數(shù)。例如,您可能希望使data
prop 的名稱可配置,以進一步將HOC與封裝組件隔離?;蛘吣梢越邮芘渲玫膮?shù)shouldComponentUpdate
,或者配置數(shù)據(jù)源的參數(shù)。這些都是可能的,因為HOC完全控制組件的定義。
與組件一樣,合約withSubscription
與包裝組件之間的合約完全基于道具。這可以很容易地將一個HOC換成另一個HOC,只要它們?yōu)榘b組件提供相同的道具。例如,如果您更改數(shù)據(jù)提取庫,這可能很有用。
抵制HOC內(nèi)部修改組件原型(或者改變它)的誘惑。
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 receivedconst EnhancedComponent = logProps(InputComponent);
這有幾個問題。一個是輸入組件不能與增強組件分開重復(fù)使用。更關(guān)鍵的是,如果你申請的另一個HOC到EnhancedComponent
那個也發(fā)生變異componentWillReceiveProps
,第一HOC的功能將被改寫!這個HOC也不能用于沒有生命周期方法的函數(shù)組件。
突變HOC是一個漏洞抽象 - 消費者必須知道它們是如何實施的,以避免與其他HOC發(fā)生沖突。
通過將輸入組件包裝在容器組件中,HOC不應(yīng)該使用變異,而應(yīng)該使用組合:
function logProps(WrappedComponent) { return class extends React.Component { componentWillReceiveProps(nextProps) { console.log('Current props: ', this.props); console.log('Next props: ', nextProps); } render() { // Wraps the input component in a container, without mutating it. Good! return <WrappedComponent {...this.props} />; } }}
這個HOC具有與變種版本相同的功能,同時避免了沖突的可能性。它與類和功能組件一樣有效。而且因為它是一個純粹的功能,它可以與其他HOC組合,甚至可以與其自身組合。
您可能已經(jīng)注意到HOC和稱為容器組件的模式之間的相似之處。集裝箱組件是在高層和低層關(guān)注點之間分離責任戰(zhàn)略的一部分。容器管理諸如訂閱和狀態(tài)之類的東西,并將道具傳遞給處理諸如呈現(xiàn)UI之類的事物的組件。HOC使用容器作為其實施的一部分。您可以將HOC視為參數(shù)化容器組件定義。
HOC向組件添加功能。他們不應(yīng)該大幅改變合同。預(yù)計從HOC返回的組件具有與被包裝組件類似的接口。
HOC應(yīng)該通過與其特定關(guān)注無關(guān)的道具。大多數(shù)HOC包含一個類似于下面的渲染方法:
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} /> ); }
此慣例有助于確保HOC盡可能靈活且可重用。
并非所有HOC看起來都一樣。有時他們只接受一個參數(shù),包裝組件:
const NavbarWithRouter = withRouter(Navbar);
HOC通常會接受其他參數(shù)。在Relay的這個例子中,一個配置對象被用來指定一個組件的數(shù)據(jù)依賴關(guān)系:
const CommentWithRelay = Relay.createContainer(Comment, config);
HOC最常見的簽名如下所示:
// React Redux's `connect`const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
什么?!如果你把它分開,很容易看到發(fā)生了什么。
// connect is a function that returns another functionconst enhance = connect(commentListSelector, commentListActions); // The returned function is an HOC, which returns a component that is connected // to the Redux storeconst ConnectedComment = enhance(CommentList);
換句話說,connect
是一個返回高階組件的高階函數(shù)!
這種形式可能看起來很混亂或不必要,但它有一個有用的特性。單參數(shù)HOC(如connect
函數(shù)返回的HOC)具有簽名Component => Component
。輸出類型與輸入類型相同的函數(shù)非常容易組合在一起。
// Instead of doing this...const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ... you can use a function composition utility // compose(f, g, h) is the same as (...args) => f(g(h(...args)))const enhance = compose( // These are both single-argument HOCs withRouter, connect(commentSelector))const EnhancedComponent = enhance(WrappedComponent)
(這個屬性也允許使用connect
其他增強器樣式的HOC作為裝飾器,這是一個實驗性JavaScript提案。)
所述compose
效用函數(shù)是由許多第三方庫包括lodash(如提供lodash.flowRight
),終極版,和Ramda。
由HOC創(chuàng)建的容器組件像任何其他組件一樣出現(xiàn)在React Developer Tools中。為了便于調(diào)試,選擇一個顯示名稱來傳達它是HOC的結(jié)果。
最常用的技術(shù)是封裝包裝組件的顯示名稱。因此,如果您的高階組件被命名withSubscription
,并且包裝組件的顯示名稱是CommentList
,則使用顯示名稱WithSubscription(CommentList)
:
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription;}function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }
如果您是React的新手,那么高階組件會附帶一些注意事項,這些注意事項不會立即顯現(xiàn)出來。
React的差異算法(稱為reconciliation)使用組件標識來確定它是應(yīng)該更新現(xiàn)有的子樹還是將其丟棄并掛載新的子樹。如果返回的組件與來自先前渲染的組件render
相同(===
),則React通過用新組件區(qū)分它來遞歸更新子樹。如果不相等,則前一個子樹完全卸載。
通常情況下,你不需要考慮這一點。但是它對于HOC很重要,因為它意味著你無法將HOC應(yīng)用到組件的渲染方法中的組件:
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 />; }
這里的問題不僅僅是性能 - 重新安裝組件會導(dǎo)致組件及其所有子組件的狀態(tài)丟失。
相反,在組件定義之外應(yīng)用HOC,以便只生成一次結(jié)果組件。那么,它的身份將在整個渲染過程中保持一致。無論如何,這通常是你想要的。
在您需要動態(tài)應(yīng)用HOC的罕見情況下,您也可以在組件的生命周期方法或其構(gòu)造函數(shù)中執(zhí)行此操作。
有時在React組件上定義靜態(tài)方法很有用。例如,中繼容器公開了一個靜態(tài)方法getFragment
來促進GraphQL片段的組合。
但是,如果將HOC應(yīng)用于組件,則原始組件將使用容器組件進行包裝。這意味著新組件沒有任何原始組件的靜態(tài)方法。
// Define a static methodWrappedComponent.staticMethod = function() {/*...*/} // Now apply an HOCconst EnhancedComponent = enhance(WrappedComponent); // The enhanced component has no static methodtypeof EnhancedComponent.staticMethod === 'undefined' // true
為了解決這個問題,你可以在返回之前將這些方法復(fù)制到容器中:
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // Must know exactly which method(s) to copy :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance;}
但是,這需要您確切地知道需要復(fù)制哪些方法。您可以使用hoist-non-react-statics來自動復(fù)制所有非React靜態(tài)方法:
import hoistNonReactStatic from 'hoist-non-react-statics';function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance;}
另一種可能的解決方案是將靜態(tài)方法與組件本身分開導(dǎo)出。
// Instead of...MyComponent.someFunction = someFunction;export default MyComponent; // ...export the method separately...export { someFunction }; // ...and in the consuming module, import bothimport MyComponent, { someFunction } from './MyComponent.js';
雖然高階組件的慣例是將所有道具傳遞給包裝組件,但不可能通過參考。這是因為ref
它不是一個真正的道具key
,它是由React專門處理的。如果將ref添加到其組件是HOC結(jié)果的元素,則ref引用最外層容器組件的實例,而不是包裝組件。
如果你發(fā)現(xiàn)自己面臨這個問題,理想的解決方案是找出如何避免使用ref
。偶爾,剛剛接觸React范例的用戶依賴于在支撐物更好地工作的情況下的參考。
也就是說,有些時候refs是必要的逃生艙口,否則React不會支持它們。聚焦輸入字段是一個例子,您可能需要對組件進行必要的控制。在這種情況下,一種解決方案是通過給它一個不同的名稱來傳遞一個ref回調(diào)作為普通道具:
function Field({ inputRef, ...rest }) { return <input ref={inputRef} {...rest} />;} // Wrap Field in a higher-order componentconst EnhancedField = enhance(Field); // Inside a class component's render method...<EnhancedField inputRef={(inputEl) => { // This callback gets passed through as a regular prop this.inputEl = inputEl }}/>// Now you can call imperative methodsthis.inputEl.focus();
這不是一個完美的解決方案。我們更喜歡參考資料仍然是圖書館關(guān)注的問題,而不是要求您手動處理它們。我們正在探索解決這個問題的方法,以便使用HOC是不可觀測的。