暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

技术分享 | React Hooks 封装指南

云趣科技 2021-06-11
2621

点击上方蓝字关注我们



Why React Hooks?


从v16.8开始,React 就引入了函数式编程的概念,而放弃 class 转而使用 function ,最大的动机便是 class 让组件本身变得臃肿,代码本身的可读性、维护性成为了瓶颈。且在传统的架构中逻辑复用(带有组件生命周期)一直都是 class 组件的痛点,无论是 Mixin 还是后来的 Higer order components + Decorator 模式的实现都不尽如人意,由此 Hooks 便诞生了。

本文将会采用理论+代码实战的方式让大家更快地了解 Hooks 的优势,并且如何在项目中合理引入使用。

新旧方法对比

本文前序中提到,React 团队放弃 class 写法主要有几点。一是 class 件比较臃肿难以理解,例如里面还是存在较多的固定语法,包括生命周期函数 componentDidMount 、 componentDidUpdate 等等,使用者需要时刻牢记每个生命周期的副作用以及使用场景。二是 class 类型组件对于逻辑复用有较大的限制。

例如:我们实现一个组件挂载 (mounted) 后更改浏览器标签页标题,并渲染标题至一个 div 中的抽象方法。





Mixin

    import React from 'react';


    var titleMixin = {
    componentDidMount: function() {
    window.document.title = this.props.title
    }

    renderTitle: function() {
    return <div>{this.props.title}</div>
    }
    }


    React.createClass({
    mixins: [titleMixin],


    render(){
    return {this.renderTitle()}
    }

    这里可以看到,Mixin 写法是直接在 class 组件生成的时候直接注入进来的,并且按照该写法,我们可以了解到组件是可以一次性注入多个 Mixin 的,这就带来一个问题,这些 Mixin 所带来的生命周期、props、function都会混入到这个 class 中,随着项目体积的逐渐增大,所抽象出来的逻辑越来越多,这种暴力注入的方式难免会带来命名冲突并且会破坏原来组件的封装,引入了更多不确定元素,当需要找到一个方法的来源时,往往需要 review 所有 Mixin 的方法来判断。

     




    高阶组件(HOC)

      import React from 'react';


      const setTitle = (WrappedComponent) => {
      class WrapperComponent extends Component {
      componentDidMount: function() {
      window.document.title = this.props.title
      }

      renderTitle = () => {
      return <div>{this.props.title}</div>
      }

      render() {
      <WrappedComponent {...this.props, renderTitle} >
      }
      }


      return WrapperComponent
      }


      @setTitle
      class Test extends React.Componet {
      render() {
          return {this.props.renderTitle()}
        }

      可以看到,高阶组件对于逻辑的侵入比起 Mixin 要小不少,但还是避免不了地对子组件的 props 对象造成了入侵(尤其是 props 拦截),搭配装饰器虽然能让整体复用写法更加优雅,但如果业务组件复用了很多逻辑,那头上就会出现一堆的装饰器, props 中方法的来源依旧混乱,到时还是需要 review 所有的装饰器来找到数据来源。且无限追加高阶组件势必会造成 props 中命名冲突覆盖,也是项目中需要注意的一点,对于实际项目依旧不够友好。





      Hooks

        import * as React from 'react';


        const useTitle = (title: string) => {
        React.useEffect(() => {
        window.document.title = title
        }, [])

        const renderTitle = () = {
        return <div>{title}</div>
        }

        return [renderTitle]
        }


        const ComponentTest: React.FC<any> = (props: any) => {
        const [renderTitle] = useTitle(props.title)

        return renderTitle()
        }


        Hooks 对比其他两类写法则要简单很多,这里观察到不仅仅是在生命周期声明上的简化(使用了 Effect Hook 来对标 componentDidMount 声明周期),在逻辑复用方面也是完全透明、逻辑可循的,不会影响到原来的组件的 props,state 。且在函数组件中弃用了 this 写法,所暴露出的复用方法也在 Hook 的返回值中体现,配合上 Typescript 的强类型校验,真正做到了无侵入式的逻辑复用特性。

         

        自定义Hook


        通过上述的对比,想必大家已经了解三者之间的优劣以及时代的发展。但上文中列举的例子都是较为简单且实用度不高的。除了在函数式编程中经常用到的几个 Hook 比如 useState useEffect 等,还可以通过自定义的方式来编写业务Hook来满足达到逻辑复用的效果。在该段落中我们就编写一个 api 请求的 hook 的 demo。

        在现在的业务模式中,通常我们进入一个页面都会在 mount 阶段发出一个 api 请求,而该请求通常是有一个特定的返回格式的,我司使用的便是  {msg: string; err_code: number; data: any} 的固定返回格式,请求返回的具体数据都会放置在data字段中,而其他请求基本的报错信息则会放置在其他两个字段中,且在请求的过程中需要联动页面做出不同的反馈,可分为几个状态:pending、success、error。现在我们就基于该场景做一个简单的封装:

          import * as React from 'react'
          // 声明Promise返回的api类型
          type ApiType<T, K extends any[]> = (...arg: K) => Promise<T>
          // 基础返回类型
          interface BasePromiseCallback<T extends any> {
          error_code?: number
          msg?: string
          data?: T
          }
          // Hooks封装过后的数据返回类型
          type FetchHookRes<T extends BasePromiseCallback<any>> = {
          data: T['data']
          status: 'loading' | 'error' | 'success'
          message?: string
          errorCode?: number
          }


          const useFetch = <T extends BasePromiseCallback<any>, K extends Array<any>>(api: ApiType<T, K>, arg: K, defaultRes: any) => {
          const [reIndex, setReIndex] = React.useState<number>(1)
          const [result, setResult] = React.useState<FetchHookRes<T>>({
          data: defaultRes,
          status: 'loading',
          })

          // 监听请求参数和重新请求动作统计,都可再次出发该请求
          React.useEffect(() => {
          api(...arg).then(({ err_code, msg, data }) => {
          // 请求返回成功
          if (err_code === 0) {
          setResult({
          data: data,
          status: 'success'
          })
          } else {
          setResult({
          data: data,
          status: 'error',
          errorCode: err_code,
          msg: msg
          )}
          }
          }).catch((e) => setRestult({ ...result, status: 'error', msg: e }))
          }, [JSON.stringify(param.arg), reIndex])

          function refresh() {
          setReIndex(reIndex + 1)
          }

          return [result, refresh]
          }
          // 调用组件
          const TestComponent = () => {
          // 调用Hook并传入封装好的promise方法及对应参数,返回值为结果、状态及刷新方法
          const [result, refresh] = useFetch(apiPromise, argxxx, [])

          return (
          <div>
          <span>status: {result.status}</span>
          {result.map((item, index) => (
          <div onClick={() => refresh()}>Edited item{index}</div>
          ))}
          </div>
             )
           }

          以上我们便实现了一个简易的带有 TS 静态提示的请求 Hook ,该 Hook 可以帮助我们在挂载阶段就需要数据请求的组件中,直接调用该 Hook 来请求数据并管理该请求的进行时状态,有点类似 Suspense 的实现。当然,我们完全可以基于该 Hook 做进一步的封装,例如去实现一个轮询的请求,加入请求次数统计及状态判断形成队列防止重复请求,加入更多的个性化参数如各阶段的 callback ,delay 等满足更多的实际场景,最后贴出云趣科技封装的 useFetch Hook 供大家参考。


          useFetch Hook

            import * as React from 'react'
            import { noop } from 'lodash'


            import { Noop, BasePromiseCallback, DATA_GET_STATUS, BasePromiseCallbackMsg } from '@/typings/common'


            export type ApiType<T, K extends any[]> = (...arg: K) => Promise<T>
            export type FetchDispatch<T> = React.Dispatch<React.SetStateAction<FetchHookRes<T>>>
            export type FetchHookParams<T extends BasePromiseCallback<any>, K extends Array<any>> = {
            // 请求所关联的api,当传入格式为数组时将会视为有多个请求联动或并发情况,不触发默认逻辑
            // 默认情况下,一个hook对应一个请求
            api: ApiType<T, K> | ApiType<T, K>[]
            // 请求所需要的参数,必须为 Array 类型
            arg: K
            // 请求返回默认值,注意该值非常重要,在某些请求发出前可能需要该hook默认值直接渲染页面,所以这里暴露出一个参数指定其默认值
            defaultRes?: T['data']
            // 是否在参数改变后重新触发请求
            reload?: boolean | (() => boolean)
            是否直接发送请求
            send?: boolean | (() => boolean)
            是否在重新触发请求后清空上一次的数据
            cleanData?: boolean
            预处理函数,方便对请求回来的数据做进一步处理,之所以暴露出接口为了防止函数rerender时重复触发,该函数保证只有请求回调时才会触发
            如果在该函数中改变了原有的数据结构,请在返回的 data 值中自行使用 as 赋予新的类型
            preprocess?: (value: T['data']) => any
            // 成功回调
            successCallback?: (value: T['data']) => void
            // 失败回调
            errorCallback?: (errorCode: number, message: any) => void
            // 最终回调
            finalCallback?: (func: FetchDispatch<T>) => void
            // 用于自定义其请求形态,由此可以兼容 请求依赖联动 问题以及同时请求问题(promise.all), 所提供的参数可以用于控制请求的返回值、请求的状态
            // 前提条件为,传入的api必须是一个数组形式,否则无法触发该自定义函数
            customize?: (value: { api: ApiType<T, K>[]; arg: K; res: FetchHookRes<T>; dispatch: FetchDispatch<T> }) => void
            }
            export type FetchHookRes<T extends BasePromiseCallback<any>> = {
            data: T['data']
            status: DATA_GET_STATUS
            message?: BasePromiseCallbackMsg
            errorCode?: number
            }


            const initParam: FetchHookParams<any, any> = {
            api: null,
            arg: null,
            defaultRes: null,
            reload: false,
            send: true,
            cleanData: false,
            preprocess: null,
            successCallback: noop,
            errorCallback: noop,
            finalCallback: noop,
            customize: null,
            }
            const useFetch = <T extends BasePromiseCallback<any>, K extends Array<any>>(
            param: FetchHookParams<T, K>
            ): [FetchHookRes<T>, Noop, number] => {
            const {
            api,
            arg,
            defaultRes,
            reload,
            send,
            cleanData,
            preprocess,
            successCallback,
            errorCallback,
            finalCallback,
            customize,
            } = {
            ...initParam,
            ...param,
            }
            const [reIndex, setReIndex] = React.useState<number>(1)
            const [result, setResult] = React.useState<FetchHookRes<T>>({
            data: defaultRes,
            status: 'loading',
            })
            const count = React.useRef(0)
            const refresh = () => setReIndex(reIndex + 1)
            // Effect中监听三个值,分别为
            // arg:该请求的依赖参数,当改变时将会重新触发生命周期
            // index:请求触发的次数,当使用refresh回调函数时将会改变其值
            // send:请求是否立即发出,可以限制某些场景下对请求发送的控制
            React.useEffect(() => {
            const currentCount = count.current


            if (send) {
            if (reload) {
            setResult({
            data: cleanData ? defaultRes : result.data,
            status: 'loading',
            })
            }


            if (api instanceof Array) {
            customize && customize({ api: api, arg: arg, res: result, dispatch: setResult })
            } else {
            api(...arg)
            .then(({ data, error_code, msg }) => {
            // 记录触发次数,如果页面已经被卸载将不会触发setState造成性能损耗,避免React警告
            if (currentCount !== count.current) {
            return
            }




            if (error_code === 0) {
            const newData = typeof preprocess === 'function' ? preprocess(data) : data




            setResult({ data: newData, status: 'success' })
            successCallback(newData)
            } else {
            setResult({
            data: defaultRes,
            status: 'error',
            errorCode: error_code,
            message: msg,
            })
            errorCallback(error_code, msg)
            }
            })
            .catch((e) => {
            setResult({ ...result, status: 'error', message: e })
            errorCallback(-1, e)
            })
            .finally(() => finalCallback(setResult))
            }
            }


            return () => {
            count.current += 1
            }
            }, [JSON.stringify(param.arg), reIndex, send])
            // hook返回值有三个,通过数组形式返回,切记顺序固定且无法改变
            // 返回值分别为 返回值、重新触发请求函数、请求发送次数统计
            return [result, refresh, reIndex]
            }


            export default useFetch




            作者:钟灵

            简介:云趣科技全栈工程师

            出品:云趣科技




            云趣 ,等您关注





            文章转载自云趣科技,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论