一.什么是Hoc
HOC(Higher-order component)是一种React 的进阶使用方法,只要还是为了便于组件的复用。
强调一点,HOC本身并不是 React API, 它就是一个方法,一个接收一个组件作为参数,返回一个增强的组件的方法。
二.什么时候使用HOC?
高阶组件的目的是解决一些交叉问题(Cross-Cutting Concerns)。而最早时候 React 官方给出的解决方案是使用 mixin 。
但官方也意识到了使用mixins技术来解决此类问题所带来的困扰远高于其本身的价值。更多资料可以查阅官方说明。
在React开发过程中,在很多情况下,组件需要被“增强”(enhanced),比如给组件添加或修改一些特定的属性,针对多个组件的代码复用。
概括的讲,HOC能够实现:
1. 代码复用,代码模块化
2. 渲染劫持, 操作state
3. Props 增删改
三.高阶组件的用途
高阶组件是一个函数,它会返回一个新的组件,这样有什么用呢?
我们来看Web前端最基本的场景:我们有一个图书列表,需要用Ajax请求数据,然后在页面上把它展现出来。
方案1.0 - 简单直接
第一种方案最直接:在组件mounted之后,执行Ajax请求,待取到数据之后,更新状态,触发组件更新,渲染数据到UI上面。
核心代码是这样的:
const BookList = () => {
const [bookList, setBookList] = useState([]);
useEffect(() => {
Ajax.get('data/books').then(res => setBookList(res));
}, []);
return (
<ul> {bookList.map(book => ( <li>{book.name}({book.author})</li> ))} </ul>
);
};
这个方案跑起来没啥问题,不过略显粗糙,没有分层,耦合太重。如果我们要加图书评论列表、相似图书推荐列表等等功能,那么就要再写几个看起来差不多的组件。模型和状态部分的代码几乎是重复的。
方案2.0 - 高阶组件
高阶组件可以传入一个无状态组件,返回一个新组件,那么我们可以把通用的模型和状态封装在新组件里面,通过子组件的形式把模型状态通过props传入被包装的组件,这样就达到抽象通用功能的目地。
核心代码很简单:
const WithModel = (WrappedComponent, modelPath) => {
const WrappedComponentWithModel = props => {
const [data, setData] = useState(null);
useEffect(() => {
Ajax.get('data/' + modelPath).then(res => setData(res));
}, []);
return <WrappedComponent {...props} data={data} />;
}
return WrappedComponentWithModel;
};
const BookList = props => {
const { data: bookList } = props;
return (
<ul> {(bookList || []).map(book => ( <li>{book.name}({book.author})</li> ))} </ul>
);
};
const BookComments = props => {
const { data: bookComments } = props;
return (
<ul> {(bookComments || []).map(comment => ( <li>{comment.content} from {comment.author}</li> ))} </ul>
);
};
const BookListWithModle = WithModel(BookList);
const BookCommentsWithModle = WithModel(BookComments);
我们可以很方便地在模型层增加功能,比如Ajax响应结果的检查。
还存在待改进的地方:
- 现在可以查询数据了,如果还需要发送数据怎么办?比如添加一条图书记录、添加一条新的评论。
- 如果另外一个组件也要展示图书列表,那两个组件里面的数据是不是就重复了?
- 如果两个组件都展示图书列表,但是用两份数据,那么一个组件刷新了,会不会两个组件展示的数据不一致?
- 还有一个隐藏问题,如果在Ajax请求数据时,组件销毁了,状态还会更新吗?会有问题吗?
方案3.0 - 高阶组件工厂
高阶组件工厂,参考自设计模式中的工厂模式,我们通过给定的设置让组件工厂去生产组件,是方案2.0的进一步抽象。
看核心代码,重点地方会给出注释说明:
import React, { useState, useEffect } from 'react';
export function useModelStore() {
const Ajax = (() => {
const books = [{ name: 'Book1', content: 'About book', author: 'Alex' }];
const getBooks = () => {
return new Promise(resolver => {
setTimeout(resolver(books), 200);
});
};
const addBook = () => {
return new Promise(resolver => {
const No = Math.round(Math.random() * 1000);
books.push({ name: `Book${No}`, content: `About book${No}`, author: 'Alex' });
setTimeout(resolver(200), 200);
});
};
return {
get: getBooks,
post: addBook,
};
})();
// 这里先定义一个图书Model Hooks函数,用于工厂创建Model Hooks
const BookModelHooks = initialState => {
const [bookList, setBookList] = useState(initialState);
const [pending, setPending] = useState(false);
const load = async params => {
setPending(true);
Ajax.get('data/books', params)
.then(res => setBookList(res))
.finally(() => setPending(false));
};
const addBook = async params => {
Ajax.post('data/books/add', params).then(() => load());
};
// 返回Model相关的state和state操作
return [bookList, { pending, load, addBook }];
};
// 顶层状态仓库
const ModelStore = () => {
let $store = {};
const createStore = (modelName, ModelHooks, initialState) => {
$store = Object.assign({}, $store, { [modelName]: ModelHooks(initialState) });
};
const getStore = modelName => $store[modelName];
return { createStore, getStore };
};
const modelStore = ModelStore();
modelStore.createStore('bookModel', BookModelHooks, []);
return modelStore;
}
// 这里就是高阶组件工厂,工厂会生产出一个可以注入图书Model的高阶组件
const WithModelFactory = modelName => {
// 返回高阶组件
return WrappedComponent => {
const WrappedComponentWithModel = props => {
const { modelStore, ...passThroughProps } = props;
return <WrappedComponent {...passThroughProps} model={modelStore.getStore(modelName)} />;
};
return WrappedComponentWithModel;
};
};
const BookList = props => {
const { model } = props;
const [bookList, { load, addBook }] = model || [];
useEffect(() => {
load && load();
}, []);
return (
<> <ul> {(bookList || []).map(book => ( <li> {book.name}({book.author}) </li> ))} </ul> <button type="button" onClick={addBook}>Add+</button> </>
);
};
const BookComments = props => {
const { model } = props;
const [bookList] = model || [];
return (
<ul> {(bookList || []).map(comment => ( <li> {comment.content} from {comment.author} </li> ))} </ul>
);
};
export const BookListWithModel = WithModelFactory('bookModel')(BookList);
export const BookCommentsWithModel = WithModelFactory('bookModel')(BookComments);