听说你还在用HOC?一键改成Hooks行不行

2,783 阅读6分钟

前言

在React的使用中,开发者总是以一种“懒惰”的精神来进行着组件化,模块化的工作,从最开始的mixins,到HOC,render props,无一不是为了这个目的而奋斗,可是它们又有弊病,从16.8开始,React Hooks横空出世,HOC的多层嵌套,props的覆盖等问题也被拎了出来,那么,该如何从HOC过渡到Hooks呢。

什么是高阶组件(HOC)

官方文档和个人理解

react官方文档中是这么定义HOC的:

高阶组件(HOC)是 React 中可复用组件逻辑的一种高级技巧。 HOC 自身不是 React API 的一部分,他是一种基于 React 的组合特性而形成的设计模式。

高阶组件本身是一个函数,能提供的功能也和函数相同,即输入与输出,它通过对输入组件和其他参数的处理,输出一个新的具有我们所需的通用数据和方法的组件。

一个简单的需求

现在网课比较火,就用网课的作业平台作为一个例子,设想这个一个需求,需要将老师布置的作业以列表的方式显示出来,如果当前用户是老师,则增加添加作业的功能,大约会写出这样的代码:

class HomeWorkList extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      list: []
    }
  }
  
  getList = () => {
    http.get('/homework/list')
    	.then(result => {
      	if (result && result.success) {
         this.setState({
           list: result.data,
         })
         return;
        }
      	console.log('error:', result)
    	})
    	.catch(ex => {
      	console.log('ex:', ex)
    	})
  }
  /**
  * @param {number} type 标志更新类型,1 为新增,2 为修改
  * @param {object} data 需要更新的数据
  */
  updateList = (type, data) => {
    if (type === 1) {
	    this.setState({ list: this.state.list.concat(data) })
    } else if (type === 2) {
      this.setState({ 
        list: this.state.list.map(item => 
          item.id === data.id ? data : item)
      	})
    }
  }
  
  componentDidMount() {
    this.getList();
  }
  render() {
    const { isTeacher } = this.props;
    return (
    	<LayoutContainer>
      	{
          this.state.list.map(item => (
          	<HomeWorkItem
              data={item}
              isTeacher={isTeacher}
              update={data => this.updateList(2, data)}
            />
            {/* 该组件提供修改方法,修改成功后调用update操作 */}
          ))
        }
        {
          isTeacher ? (
          	<AddHomeWork update={data => this.updateList(1, data)} />
            {/* 该组件提供新增方法,新增成功后,调用update操作 */}         
          ) : null
        }
      </LayoutContainer>
    )
  }
}

以上代码中规中矩,没有什么特别的地方,也不太值得被挑剔,所以就到此为止了吗?

另一个简单的需求

但是,这时候,你发现学生提交的作业列表的需求也是将所有学生提交的作业进行列表呈现,为老师提供每条作业的评分,为未提交作业的学生提供提交作业的入口时,你会写出和上面雷同的代码:

class SubmitList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      list: []
    }
  }
  getList = () => {
    http.get('/result/list/')
    	.then(result => {
      	// 省略部分判断
      	this.setState({ list: result.data })
    	})
  }
  updateList = (type, data) => {
    // 同上
  }
  
  render() {
    const { isTeacher, isStudent, submited } = this.props;
    return (
    	<LayoutContainer>
      	{
          this.state.list.map(item => (
          	<ResultItem data={item} update={data => this.updateList(2, data)} />
          ))
        }
        {
          isStudent && !submited ?
            <SubmitHomework update={data => this.updateList(1, data)} /> :
          	null
        }
      </LayoutContainer>
    )
  }
}

可以看出,这两个组件之间存在有很多相同的代码,如果这时候还有类似的列表需求,还需要写出很多类似的重复代码,这种情况下,考虑将公共部分拆为HOC。

高阶组件改造

const withList = ({ url }) => RenderComponent => {
	return class extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        list: []
      }
    }
    getList = () => {
      http.get(url)
      	.then(result => {
        	if (result.success) {
	        	this.setState({ list: result.data })
          } else {
            console.log('error:', result)
          }
      	})
      	.catch(ex => {
        	console.log('ex:', ex)
      	})
    }
    updateList = (type, data) => {
      if (type === 1) {
        this.setState({ list: this.state.list.concat(data) })
      } else if (type === 2) {
        this.setState({ 
          list: this.state.list.map(item => 
            item.id === data.id ? data : item)
          })
      }
    }
    componentDidMount() {
      this.getList()
    }
    render() {
      const { list } = this.state
      return <RenderComponent list={list} update={this.update} />
    }
  } 
}

这时候,就可以很简单的实现上面两个功能和其类似功能了,我一般使用装饰器(注解)的方式:

// 作业列表
@withList({ url: '/homework/list' })
class HomeWorkList extends React.Component {
  render() {
  	const { list, isTeacher, update } = this.props;
    return (
    	<LayoutContainer>
      	{
         	list.map(item => (
          	<HomeWorkItem
              data={item}
              isTeacher={isTeacher}
              update={data => update(2, data)}
            />
            {/* 该组件提供修改方法,修改成功后调用update操作 */}
          ))
        }
        {
          isTeacher ? (
          	<AddHomeWork update={data => update(1, data)} />
            {/* 该组件提供新增方法,新增成功后,调用update操作 */}         
          ) : null
        }
      </LayoutContainer>
    )
  }
}

// 提交作业列表
@withList({ url: '/homework/result' })
class ResultList extends React.Component {
  render() {
    const { isTeacher, isStudent, submited, update, list } = this.props;
    return (
    	<LayoutContainer>
      	{
          list.map(item => (
          	<ResultItem data={item} update={data => update(2, data)} />
          ))
        }
        {
          isStudent && !submited ?
            <SubmitHomework update={data => update(1, data)} /> :
          	null
        }
      </LayoutContainer>
    )
  }
}

这样,对于增删改查的类似需求,就不再需要每次写一堆相同的冗余代码,而只需要使用HOC对相应内容进行项进行渲染就可以了。

HOC出现之前,一般使用mixin的方式,针对mixin所带来的一系列问题,早已达成共识,这里就不再赘述。

最后,以上🌰中的更新数据也可以拆为高阶组件,这里就不再赘述,下面,来看一下关于React Hooks的内容。

什么是React Hooks

文档解读

官方文档说,Hook 是 react 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hook的中文意思是钩子,其中一共有四个常用的钩子,分别是:

useState()
useContext()
useReducer()
useEffect()

以上几个钩子顾名思义,很容易知道其意思。

比如useState是一个状态钩子,针对一个纯函数组件(木偶组件,也就是 dumb 组件),是没有 state 的,所以将其状态放在钩子下面。

useContext是一个共享状态钩子,context 本身是一个组件顶层 API ,这个钩子的作用就是让你在不使用 connect 的情况下,直接订阅 Context。

useReducer 则是一个 action 钩子,React 本身不提供状态管理功能,通常使用 Redux 来做状态管理,这个钩子则是引入了 Redux 中 reducer 功能。

userEffect 和字面意思一致,是一个副作用钩子,在前端,最常见的副作用是向服务端请求数据,这个组件可以代替 class 组件中的 componentDidMount 等功能。

用React Hooks改写HOC

一般来说,介绍hooks的文章几乎就到上面就戛然而止了,那么,怎么用 Hook 来代替 HOC 呢,简单来讲,如何用 Hook 来改写第一部分那个关于记录点击的例子呢,我写了这样的一部分代码:

import { useState, useEffect } from 'react'

export default function(url) {
    let [list, setList] = useState([]);

    useEffect(() => {
        http.get(url)
            .then(result => {
                if (result.success) {
                    setList(result.data)
                } else {
                    console.log('error:', result)
                }
            })
            .catch(ex => {
                console.log('ex:', ex)
            })
    }, []) // 这里将第二个参数设置为空,其效果与componentDidMount相同

    const update = (type, data) => {
        if(type === 1) {
            setList(list.concat(data))
        } else if (type === 2) {
            setList(list.map(item => item.id === data.id ? data : item))
        }
    }

    return [list, update]
}

用法也很简单

// 作业列表
const HomwWorkList = ({ isTeacher }) => {
  const [list, update] = useList('/homework/list')
  return (
    <LayoutContainer>
      {
        list.map(item => (
          <HomeWorkItem
            data={item}
            isTeacher={isTeacher}
            update={data => update(2, data)}
            />
          {/* 该组件提供修改方法,修改成功后调用update操作 */}
        ))
      }
      {
        isTeacher ? (
          <AddHomeWork update={data => update(1, data)} />
          {/* 该组件提供新增方法,新增成功后,调用update操作 */}         
        ) : null
      }
    </LayoutContainer>
  )
}

// 提交作业列表
const ReulstList = ({ isTeacher, isStudent, submited }) => {
  const [list, update] = useList('/homework/result')
  return (
    <LayoutContainer>
      {
        list.map(item => (
          <ResultItem data={item} update={data => update(2, data)} />
        ))
      }
      {
        isStudent && !submited ?
          <SubmitHomework update={data => update(1, data)} /> :
        null
      }
    </LayoutContainer>
  )
}