原文:https://daveceddia.com/extract-state-with-higher-order-components/
高阶组件是对React代码进行更高层次重构的好方法,如果你想精简你的state和生命周期方法,那么高阶组件可以帮助你提取出可重用的函数。
什么是高阶组件?名字来源于高阶函数,一个函数可以接收另一个函数作为参数,并且有可能在执行后返回一个函数,这种函数就称之为高阶函数。你可能使用过高阶函数但是并没有真正意识到,例如Array.forEach
、Array.map
、setTimeout
这些都是高阶函数,我们都知道这些函数全都是接受一个函数作为参数,当新的函数返回时,他已经发生了变化。
// Ok :)
setTimeout(function() {
// do a thing after 500ms
}, 500);
// Sure...
[1, 2, 3].map(function(i) {
// multiply each element by 2
return i * 2;
});
// Wait what?
function middleware(store) {
return function(next) {
return function(action) {
// do the thing
}
}
}
那么,什么又是高阶组件呢?其实就是把一个组件接收一个组件作为参数,并返回包裹后的组件。既然可以把另一个组件作为参数,那意味着他必须是一个函数。
我们先来看一个典型的高阶组件:
// It's a function...
function myHOC() {
// Which returns a function that takes a component...
return function(WrappedComponent) {
// It creates a new wrapper component...
class TheHOC extends React.Component {
render() {
// And it renders the component it was given
return <WrappedComponent {...this.props} />;
}
}
// Remember: it takes a component and returns a new component
// Gotta return it here.
return TheHOC;
}
}
如果有两个组件都需要加载同样的数据,那么他们会有相同的 componentDidMount 函数。
//BookDetails.js
import React, { Component } from 'react';
import * as API from '../api'; // let's just pretend this exists
class BookDetails extends Component {
constructor(props) {
super(props);
this.state = {
book: null
};
}
componentDidMount() {
API.getBook(this.props.bookId).then(book => {
this.setState({ book });
})
}
render() {
const { book } = this.state;
if(!book) {
return <div>Loading...</div>;
}
return (
<div>
<img src={book.coverImg}/>
<div>{book.author}</div>
<div>{book.title}</div>
</div>
);
}
}
export default BookDetails;
// BookSummary.js
import React, { Component } from 'react';
import * as API from '../api'; // let's just pretend this exists
class BookSummary extends Component {
constructor(props) {
super(props);
this.state = {
book: null
};
}
componentDidMount() {
API.getBook(this.props.bookId).then(book => {
this.setState({ book });
})
}
render() {
const { book } = this.state;
if(!book) {
return <div>Loading...</div>;
}
return (
<div>
<div>{book.summary}</div>
</div>
);
}
}
export default BookSummary;
每个组件中constructor
和 componentDidMount
都干着同样的事情,另外,在数据拉取时都会显示Loading...
文案,那么我们应该思考如何使用高阶组件来提取这些方法。
// BookLoader.js
import * as API from 'api'; // let's just pretend this exists
// It's a function...
function loadBook() {
// Which returns a function that takes a component...
return function(WrappedComponent) {
// It creates a new wrapper component...
class BookLoader extends React.Component {
// Here's the duplicated code from above:
constructor(props) {
super(props);
this.state = {
book: null
};
}
componentDidMount() {
API.getBook(this.props.bookId).then(book => {
this.setState({ book });
})
}
render() {
const { book } = this.state;
if(!book) {
return <div>Loading...</div>;
}
// Notice how "book" is passed as a prop now
return (
<WrappedComponent
{...this.props}
book={book} />
);
}
}
// Remember: it takes a component and returns a new component
// Gotta return it here.
return BookLoader;
}
}
export default loadBook;
在 BookLoader 高阶组件中处理 book
state,并且作为prop传递给已包裹的组件,使用相同的办法来处理Loading
state,我们需要做的是拉取state,并且更新到组件中去。
把 BookDetails
和 BookSummary
组件应用 到新的BookLoader
高阶组件中去:
// BookDetails.js
import React, { Component } from 'react';
import loadBook from './BookLoader';
class BookDetails extends Component {
render() {
// Now "book" comes from props instead of state
const { book } = this.props;
return (
<div>
<img src={book.coverImg}/>
<div>{book.author}</div>
<div>{book.title}</div>
</div>
);
}
}
export default loadBook()(BookDetails);
// BookSummary.js
import React, { Component } from 'react';
import loadBook from './BookLoader';
class BookSummary extends Component {
render() {
// Now "book" comes from props instead of state
const { book } = this.props;
return (
<div>
<div>{book.summary}</div>
</div>
);
}
}
export default loadBook()(BookSummary);
在你完成高阶组件迁移后,应该更进一步地简化你的代码,这个例子的组件比较简单,他们可以变成普通的功能:
// BookDetails.js
import loadBook from './BookLoader';
function BookDetails({ book }) {
return (
<div>
<img src={book.coverImg}/>
<div>{book.author}</div>
<div>{book.title}</div>
</div>
);
}
export default loadBook()(BookDetails);
// BookSummary.js
import loadBook from './BookLoader';
function BookSummary({ book }) {
return (
<div>
<div>{book.summary}</div>
</div>
);
}
export default loadBook()(BookSummary);