初学者看到高阶组件,可能就会有点怕,因为当年我也一样。而如今react都发布到16.13了,新的操作和理念层出不穷,还有理由不好好了解一下高端操作吗?
react组件分类
先来看下,react组件有哪几类。一是有状态组件(class组件),二是无状态组件(函数组件)。
有状态组件大家应该都比较熟悉,平时都用烂了。现在将着重看下无状态组件,理解无状态组件是理解高阶组件的基础。
无状态组件:本质是一个纯函数,用函数的形式来构造组件
纯函数:输出完全由输入来决定,没有任何副作用
render是一个纯函数,输出只和state和props有关
目前主流比较倾向于使用无状态函数组件,组件中只有props,无state
// 无状态组件
const Person = props => <div>{props.name}</div>
// 传统组件
class Person extends React.Component{
render(){
return(
<div>{props.name}</div>
)
}
}
高阶函数 vs 高阶组件
高阶函数:接收一个函数,返回一个函数
const identity = func => func
高阶组件:接收一个组件,返回一个组件
const identity = BaseComponent => BaseComponent
因为组件可以是函数形式,高阶组件本质上就是高阶函数。
描述的是接受React组件作为输入,输出一个新的React组件(增强型组件)的组件。
主要功能:封装并抽离组件的通用逻辑,让此部分逻辑更好地在组件之间复用。
高阶组件写法
根据react组件无状态和有状态的两种分类,高阶组件有两种写法。
const HOC = (BaseComponent) => class extends React.Component{
render(){
return(
<BaseComponent
{...this.props}
/>
)
}
}
const HOC = BaseComponent => props => <BaseComponent {...props} />
注意:要让props继承到返回的组件中
上手高阶组件
下面来看一些小而美的高阶组件,以此对高阶组件的写法和功能有整体的了解。
withLoading
比如现在已经有一个List列表组件,现在需要加入一个loading判断,以往我们是加在render方法里,但这种做法会改变List组件的结构。如果其他组件中又需要加入loading,这个逻辑又需要重写一遍。为此,我们可以通过写一个高阶组件把这个loading的逻辑抽离出来。
// withLoading.js
const withLoading = BaseComponent => ({isLoading, ...otherProps}) => (
isLoading ?
<div>加载中...</div> :
<BaseComponent {...otherProps}/>
)
export default withLoading;
这样,高阶组件withLoading,可以给普通组件传入属性isLoading。
在List组件中,使用withLoading组件包裹该组件,withLoading(List),这样List变成增强型组件,可以传属性isLoading
// List.js
function List (props) {
return (
<ul>
{
props.data.map((item, idx) => {
return <li key={idx}>{item}</p></li>
})
}
</ul>
)
}
export default withLoading(List)
// app.js
<List data={list} isLoading={false}></List> // 返回加载中...
<List data={list} isLoading={true}></List> // 返回List组件
withLocalStorage
在组件中,我们经常需要跟localStorage交互,可以做一个withLocalStorage的高阶组件,将交互的逻辑抽离出来。
const withLocalStorage = BaseComponent => props => {
let getLocalStorage = key => localStorage.getItem(key)
let setLocalStorage = (key, value) => localStorage.setItem(key, value)
let removeLocalStorage = key => localStorage.removeItem(key)
return <BaseComponent
getLocalStorage={getLocalStorage}
setLocalStorage={setLocalStorage}
removeLocalStorage={removeLocalStorage}
{...props}
/>
}
export default withLocalStorage;
flattenProp
现在有一个User组件
const User = props => {
return (
<div>
<div>姓名:{props.user.username}</div>
<div>年龄:{props.user.age}</div>
</div>
)
}
export default User;
// 调用
<User user={user}></User> //const user = {username: 'zhangsan', age: 20}
现在想把user属性扁平化,可以做一个flattenProp组件。
const flattenProp = propKey => BaseComponent => props => <BaseComponent {...props} {...props[propKey]} />
export default flattenProp;
针对User组件,运用高阶组件flattenProp将user属性扁平化,flattenProps('user')(User)
const User = props => {
return (
<div>
<div>姓名:{props.username}</div>
<div>年龄:{props.age}</div>
</div>
)
}
export default flattenProp('user')(User);

renameProp
同样,我们可以对props重命名。
const renameProp = (oldProp, newProp) => BaseComponent => props => {
let hoc = {
...omit(props, [oldProp]),
[newProp]: props[oldProp]
}
return <BaseComponent {...hoc} />
}
export default renameProp;
同样对于上面User组件,我们想要把username改成name,可以使用高阶组件处理,renameProp('username', 'name')(User)
const User = props => {
return (
<div>
<div>姓名:{props.name}</div>
<div>年龄:{props.age}</div>
</div>
)
}
export default flattenProp('user')(renameProp('username', 'name')(User))
上面可以注意到已经将两个高阶组件组合到一起。后面我们可以用recompose的compose方法组合多个高阶组件。
withProps
添加props
const withProps = ownerProps => BaseComponent => props => {
if (typeof ownerProps == 'function') {
ownerProps = ownerProps(props)
}
return <BaseComponent {...props} {...ownerProps}/>
}
export default withProps
withProps的参数可以是对象或函数。如果是函数,参数是父组件props,返回一个对象。
withProps({math: 90, english: 40})
withProps(({math, english, chinese}) => ({
total: math + english + chinese
}))
mapProps
过滤props,最后只保留返回的props
const mapProps = func => BaseComponent => props => {
return <BaseComponent {...func(props)}/>
}
export default mapProps
mapProps的参数是函数。函数的参数是父组件的props,返回一个对象。
mapProps(({math, english, chinese}) => ({
total: math + english + chinese
}))
除了以上操纵props外,我们还可以抽取state状态。通常用class组件setState,会带来整个页面的渲染开销。抽取state,将state放在一个小组件内部管理,而不需要在外部调用setState带来不必要的开销。
withState
添加state和更新state。基本思路是将state和更新state的函数做成props,以便调用。
const withState = (stateName, stateUpdaterName, initialState) => BaseComponent => {
const factory = React.createFactory(BaseComponent)
class withState extends React.Component {
state = {
stateValue: typeof initialState === 'function' ? initialState(this.props) : initialState
}
updateStateValue = (updateFn, callback) =>
this.setState(({stateValue}) => ({
stateValue: typeof updateFn === 'function' ? updateFn(stateValue) : updateFn
}), callback)
render () {
return factory({
...this.props,
[stateName]: this.state.stateValue,
[stateUpdaterName]: this.updateStateValue
})
}
}
return withState
}
export default withState;
withState第一个参数是state名称,第二个参数是改变state的函数名称,第三个参数是初始值。
withState('count', 'changeCount', 0)
withHandlers
处理事件交互。
const withHandlers = handlers => BaseComponent => {
const factory = React.createFactory(BaseComponent)
var mapValues = (obj, func) => {
let result = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = func(obj[key], key)
}
}
return result
}
class withHandlers extends React.Component {
handlers = mapValues(
typeof handlers === 'function' ? handlers(this.props) : handlers,
handlerFunc => (...args) => {
var handler = handlerFunc(this.props)
if (typeof handler !== 'function') {
return handler
}
return handler(...args)
}
)
render () {
return factory({
...this.props,
...this.handlers
})
}
}
return withHandlers
}
withHandlers({
handleChangeCount: props => e => {
props.changeCount(2)
}
})
// 也可以这样写
withHandlers({
handleChangeCount: props => props.changeCount(2)
})
通常将withState和withHandlers配合使用
compose(
withState('count', 'changeCount', 0)
withHandlers({
handleChangeCount: props => props.changeCount(2)
})
)
使用recompose
上面讲述的这些高阶组件,其实recompose都已经做了封装。我们可以直接使用recompose包来调用这些组件。
安装
yarn add recompose
使用
import {compose, flattenProp, renameProp, withProps, withState, withHandlers} from 'recompose'
compose(
flattenProp('user'),
renameProp('username', 'name'),
withProps({
sex: 'male'
}),
withState('count', 'changeCount', 0)
withHandlers({
handleChangeCount: props => props.changeCount(2)
})
)(User)
进入的props
{
user: {username: 'zhangsan', age: 20}
}
加工后的props
{
name: 'zhangsan',
age: 20,
sex: 'male',
count: 0,
changeCount,
handleChangeCount
}
最后
recompose还有其他不少高阶组件,有兴趣的小伙伴可以再深入研究研究。
文档:recompose
github:recompose