最后更新時間:2019/05/15
以下內(nèi)容來自:
- React 官網(wǎng)文檔
- Robin Wieruch 博客
- 《The Road to learn React Your journey to master plain yet pragmatic React.js》
自身理解:(
1 HOC 基礎(chǔ)概念
1.1 定義
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature
They take any input - most of the time a component, but also optional arguments - and return a component as output. The returned component is an enhanced version of the input component and can be used in your JSX
const EnhancedComponent = higherOrderComponent(WrappedComponent);
1.2 理解
- 高階組件只是 React 建議的一種機制、模式,并非一個特殊的 API琼讽。
- HOC 的目的在于通過將不同的 Component 中相同的邏輯提取出來,在一個 function 實現(xiàn)這些通用邏輯门躯,之后接受 Component 輸入,“注入”通用邏輯酷师,實現(xiàn)對 component 的增強讶凉,減少代碼冗余,提高組件的復(fù)用性山孔。這種通用邏輯的注入可以是向 Component 注入新的 prop懂讯,可以是對 Component 的 prop 進行某種檢查,進行條件渲染等台颠。
- HOC 的返回可以是一個 class 組件褐望,function 組件或者另外的 HOC
- 傳入的 Component 作為
return
新的增強的組件的相對獨立的一部分,因此 <span style=“text-color: red”>不要在高階組件中直接修改傳入組件(方法等)</span>
2 實例
2.1 對 Component 注入新的 prop
2.1.1 思考與使用過程
現(xiàn)在存在兩個組件串前,CommentList
與BlogPost
瘫里,他們都從一個外部數(shù)據(jù)DataSource
中獲取數(shù)據(jù)進行展示。
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() {
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>
);
}
}
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} />;
}
}
兩個組件的區(qū)別:
- 從DataSource中獲取數(shù)據(jù)的方法不同荡碾,一個是
getComments
谨读,一個是getBlogPost
- 展示數(shù)據(jù)的
render
函數(shù)不同。
相同點:
- 在組件掛載時 subscribe DataSource坛吁,當
DataSource
發(fā)生改變后劳殖,調(diào)用handleChange
重新渲染;同時卸載時移除 listener - 都從DataSource中獲取數(shù)據(jù)
可以看到這兩個組件存在相同的邏輯拨脉,即從DataSource中獲取數(shù)據(jù)哆姻,進行渲染。當中存在冗余的代碼玫膀,如果再寫第三個組件矛缨,如IssueList
,那么這個邏輯還要重復(fù)一個匆骗。
因此我們可以采用以下的高階組件提取通用邏輯:
(1)定義高階組件
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class EnhancedComponent 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 接受WrappedComponent
劳景,以及selectData
兩個參數(shù),前者是需要增強的組件碉就,后者是用來從DataSource中獲取數(shù)據(jù)的 function。
注意到 HOC 返回的是一個增強的闷串、新的 class react 組件瓮钥,具有以下幾個特征:
- local
state
中保存了通過 HOC 參數(shù)selectData
拿到的數(shù)據(jù) -
render
函數(shù)返回的還是傳入的WrappedComponent
組件的實例,并且傳入了一個新的data
屬性。 - 需要注意碉熄,
{...this.props}
桨武,保證了高階組件實例生成時定傳入的props
都能夠傳入WrappedComponent
組件。
(2)重新定義原組件
重新實現(xiàn)之前的CommentList
與BlogPost
組件锈津,此時在它們的render
函數(shù)中直接使用this.props.data
來進行渲染呀酸,不需要再與DataSource
進行交互。
class CommentList extends React.Component {
render() {
const { data, ...res } = this.props;
return (
<div>
{data.map(comment => (
<Comment comment={comment} key={comment.id} {...reas} />
))}
</div>
);
}
}
class CommentList extends React.Component {
render() {
const { data, ...res } = this.props;
return <TextBlock text={data} {...res} />;
}
}
(3)定義增強組件
const CommentListWithSubscription = withSubscription(CommentList, DataSource =>
DataSource.getComments(),
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id),
);
此時的CommentListWithSubscription
與BlogPostWithSubscription
是高階組件withSubscription
返回的新增強的 class 組件琼梆。
(4) 使用新的增強組件
class App extends Component{
...
render() {
...
return (
<div>
<CommentListWithSubscription disabled/>
<BlogPostWithSubscription />
</div>
)
}
}
注意其中的disableed
屬性會一層層的傳遞給Comment
組件性誉,傳遞過程如下:
- 首先是傳入
withSubscription
返回的EnhancedComponent
組件 render 函數(shù)中的props
- 通過 return 語句中
{...this.props}
被傳遞給<WrappedComponent />
組件 - 此時
WrappedComponent
是CommentList
,在它的 render 函數(shù)可通過{...res}
傳遞給<Component />
2.1.2 總結(jié)
可以看到這種方式的 HOC 沒有直接改變傳入的 Component茎杂,而是傳入新的prop
错览,因此,在 Component 的render
函數(shù)中可以使用新的prop
進行渲染或其它操作煌往。以上例子在React docs進一步了解倾哺。
2.2 條件渲染
現(xiàn)在存在一個ToDoList
組件
function TodoList({ todos, isLoadingTodos }) {
if (isLoadingTodos) {
return (
<div>
<p>Loading todos ...</p>
</div>
);
}
if (!todos) {
return null;
}
if (!todos.length) {
return (
<div>
<p>You have no Todos.</p>
</div>
);
}
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
可以看到有很多關(guān)于 todos 的條件渲染,我們可以嘗試把這種條件渲染的邏輯提取出來刽脖,形成下面的情況:
const withLoadingIndicator = Component => ({ isLoadingTodos, ...others }) =>
isLoadingTodos ? (
<div>
<p>Loading todos ...</p>
</div>
) : (
<Component {...others} />
); // (1)
const withTodosNull = Component => props =>
!props.todos ? null : <Component {...props} />; // (2)
const withTodosEmpty = Component => props =>
!props.todos.length ? (
<div>
<p>You have no Todos.</p>
</div>
) : (
<Component {...props} />
); // (3)
重新定義ToDoList
組件:
const TodoList = ({ todos }) => (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
之后利用這三個新的 HOC羞海,定義新的增強組件:
const TodoListWithConditionalRendering = withLoadingIndicator(
withTodosNull(withTodosEmpty(TodoList)),
);
// 可以寫成以下的形式
// const TodoListOne = withTodosEmpty(TodoList);
// const TodoListTwo = withTodosNull(TodoListOne);
// const TodoListThree = withLoadingIndicator(TodoListTwo);
現(xiàn)在,生成增強組件的實例:
...
<TodoListWithConditionalRendering isLoadingTodos={true} />
...
isLoadingTodos
屬性通過 (1)(2)(3)
層層傳遞給TodoList
組件曲管。
更多的了解可以參看A gentle Introduction to React's Higher Order Components
2.3 第三方庫 HOC 例子
讓我們來看實際的例子:
// antd Form組件的使用 參見https://ant.design/components/form-cn/#Form.create(options)
import { Form } from 'antd';
class CustomizedForm extends React.Component {}
export default (CustomizedForm = Form.create({})(CustomizedForm));
代碼中的Form.create()
方法接受一個option
參數(shù)扣猫,該參數(shù)的部分屬性如下表:
參數(shù) | 說明 |
---|---|
name |
設(shè)置表單域內(nèi)字段id 的前綴 |
onValuesChange |
任一表單域的值發(fā)生改變時的回調(diào) |
一個使用的例子如下:
const CustomizedForm = Form.create({
name: 'global_state',
onFieldsChange(props, changedFields) {
props.onChange(changedFields);
},
mapPropsToFields(props) {
return {
username: Form.createFormField({
...props.username,
value: props.username.value,
}),
};
},
onValuesChange(_, values) {
console.log(values);
},
})(CustomComponent);
Form.create()
方法返回的還是一個 HOC,這個 HOC 單獨接受一個組件輸入翘地,返回增強組件申尤,即上面的Form.create({})(CustomizedForm)
。
此時在CustomizedForm
組件中就可以使用被高階組件注入的屬性form
衙耕。例如:
// CustomizedForm
render() {
const {
getFieldDecorator, getFieldsError, getFieldError, isFieldTouched,
} = this.props.form;
// Only show error after a field is touched.
const userNameError = isFieldTouched('userName') && getFieldError('userName');
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</Form.Item>
</Form>
);
}
2.4 使用注意事項
定義的 HOC 可以采用
with
開頭-
不要在
render
中使用 HOC昧穿,而是在render
之外就使用 HOC 定義好新的增強組件,在 render 函數(shù)中直接使用 HOC 返回的增強組件橙喘。原因有以下兩點:- 效率:每次
render
執(zhí)行時时鸵,都使用 HOC 生成新的增強組件,一方面效率較低厅瞎,另一方面virtual DOM
和real DOM
比較時饰潜,新的增強組件與舊的組件不會認為是相同的。 - 增強組件的狀態(tài)丟失:每次
render
執(zhí)行會卸載之前的增強組件和簸,導(dǎo)致其中的state
丟失
- 效率:每次
-
靜態(tài)方法需要特別對待:如果在原始組件中定義了靜態(tài)方法彭雾,之后使用 HOC 返回的增強組件是沒有該靜態(tài)方法的。如:
// Define a static method WrappedComponent.staticMethod = function() { /*...*/ }; // Now apply a HOC const EnhancedComponent = enhance(WrappedComponent); // The enhanced component has no static method typeof EnhancedComponent.staticMethod === 'undefined'; // true
要解決這個問題锁保,需要拷貝該靜態(tài)方法:
function enhance(WrappedComponent) { class Enhance extends React.Component { /*...*/ } // Must know exactly which method(s) to copy :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
ref
屬性無法傳遞薯酝,原因在于ref
不是和其它普通 prop 一起存在props
中的半沽,它會被 React 特殊處理,ref
只會指向增強組件吴菠,而不是被包裹的原始 Component者填。解決這個問題在于使用React.forwardRef
, Learn more about it in the forwarding refs section.