玩转react-hooks,自定义hooks设计模式及其实践

简介: react 自定义 hooks 实践

前言

自从react16.8,react-hooks诞生以来,在工作中一直使用hooks,一年多的时间里,接触的react项目,渐渐使用function无状态组件代替了classs声明的有状态组件,期间也总结了一些心得。尤其对于近期三个月的项目里,一点点用自定义hooks来处理公司项目中重复逻辑,总体感觉还不错。今天给大家讲讲我在工作中对react-hooks心得,和一些自定义hooks的设计思想,把在工作中的经验分享给大家。

自定义hooks设计

又回到那个问题?什么是hooks。

react-hooks是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks思想和初衷,也是把组件,颗粒化,单元化,形成独立的渲染环境,减少渲染次数,优化性能。

还不明白react-hooks的伙伴可以看的另外一篇文章:
react-hooks如何使用?

什么是自定义hooks

自定义hooks是在react-hooks基础上的一个拓展,可以根据业务需要制定满足业务需要的hooks,更注重的是逻辑单元。通过业务场景不同,我们到底需要react-hooks做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义hooks产生的初衷。

如何设计一个自定义hooks,设计规范

逻辑+ 组件

hooks 专注的就是逻辑复用, 是我们的项目,不仅仅停留在组件复用的层面上。hooks让我们可以将一段通用的逻辑存封起来。将我们需要它的时候,开箱即用即可。

自定义hooks-驱动条件

hooks本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks的执行,由此可见组件本身执行和hooks的执行如出一辙。

那么prop的修改,useState,useReducer使用是无状态组件更新条件,那么就是驱动hooks执行的条件。
我们用一幅图来表示如上关系。

自定义hooks-通用模式

我们设计的自定义react-hooks应该是长的这样的。

const [ xxx , ... ] = useXXX(参数A,参数B...)

在我们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么返回什么
返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。正如下图所示

自定义hooks-条件限定

如果自定义hooks没有设计好,比如返回一个改变state的函数,但是没有加条件限定限定,就有可能造成不必要的上下文的执行,更有甚的是组件的循环渲染执行。

比如:我们写一个非常简单hooks来格式化数组将小写转成大写


import React , { useState } from 'react'
/* 自定义hooks 用于格式化数组将小写转成大写 */
function useFormatList(list){
   return list.map(item=>{
       console.log(1111)
       return item.toUpperCase()
   })
}
/* 父组件传过来的list = [ 'aaa' , 'bbb' , 'ccc'  ] */
function index({ list }){
   const [ number ,setNumber ] = useState(0)
   const newList = useFormatList(list)
   return <div>
       <div className="list" >
          { newList.map(item=><div key={item} >{ item }</div>) }
        </div>
        <div className="number" >
            <div>{ number }</div>
            <button onClick={()=> setNumber(number + 1) } >add</button>
        </div>
   </div>
}
export default index

如上述问题,我们格式化父组件传递过来的list数组,并将小写变成大写,但是当我们点击add。 理想状态下数组不需要重新format,但是实际跟着执行format。无疑增加了性能开销。

所以我们在设置自定义hooks的时候,一定要把条件限定-性能开销加进去。

于是乎我们这样处理一下。

function useFormatList(list) {
   
   
    return useMemo(() => list.map(item => {
   
   
        console.log(1111)
        return item.toUpperCase()
    }), [])
}

华丽丽的解决了如上的问题。

所以一个好用的自定义hooks,一定要配合useMemo ,useCallbackapi一起使用。

自定义hooks实战

准备工作:搭建demo样式项目

为了将实际的业务情景和自定义hooks连接在一起,我这里用 taro-h5 构建了一个移动端react项目。用于描述实际工作中用到自定义hooks的场景。

demo项目地址 : 自定义hooks,demo项目

后续会更新更多自定义hooks,或者感兴趣的同学可以关注一下这个项目,或者也可以一起维护这个项目。

项目结构

page文件夹里包括自定义hooks展示demo页面,hooks文件夹里面是自定义hooks内容。

展示效果

每个listItem记录每一个完成自定义hooks展示效果,陆续还有其他的hooks。我们接下来看看hooks具体实现。

实战一:控制滚动条-吸顶效果,渐变效果-useScroll

背景:公司的一个h5项目,在滚动条滚动的过程中,需要控制 渐变 + 高度 + 吸顶效果。

1实现效果

1 首先红色色块有吸顶效果。
2 粉色色块,是固定上边但是有少量偏移,加上逐渐变透明效果。

2 自定义useScroll设计思路

需要实现功能:

1 监听滚动条滚动。
2 计算吸顶临界值,渐变值,透明度。
3 改变state渲染视图。

好吧,接下来让我们用一个hooks来实现上述工作。

页面

import React from 'react'
import { View, Swiper, SwiperItem } from '@tarojs/components'
import useScroll from '../../hooks/useScroll'
import './index.less'
export default function Index() { 
    const [scrollOptions,domRef] = useScroll()
    /* scrollOptions 保存控制透明度 ,top值 ,吸顶开关等变量 */
    const { opacity, top, suctionTop } = scrollOptions
    return <View style={
  
  { position: 'static', height: '2000px' }} >
        <View className='white' />
        <View  id='box' style={
  
  { opacity, transform: `translateY(${top}px)` }} >
            <Swiper
              className='swiper'
            >
                <SwiperItem className='SwiperItem' >
                    <View className='imgae' />
                </SwiperItem>
            </Swiper>
        </View>
        <View className={suctionTop ? 'box_card suctionTop' : 'box_card'}>
            <View
              style={
  
  {
                    background: 'red',
                    boxShadow: '0px 15px 10px -16px #F02F0F'
                }}
              className='reultCard'
            >
            </View>
        </View>
    </View>
}

我们通过一个scrollOptions 来保存透明度 ,top值 ,吸顶开关等变量,然后通过返回一个ref作为dom元素的采集器。接下来就是hooks如果实现的。

useScroll

export default function useScroll() {
   
   
 const dom = useRef(null)
  const [scrollOptions, setScrollOptions] = useState({
   
   
    top: 0,
    suctionTop: false,
    opacity: 1
  })

  useEffect(() => {
   
   
    const box = (dom.current)
    const offsetHeight = box.offsetHeight
    const radio = box.offsetHeight / 500 * 20
    const handerScroll = () => {
   
   
      const scrollY = window.scrollY
      /* 控制透明度 */
      const computerOpacty = 1 - scrollY / 160
      /* 控制吸顶效果 */
      const offsetTop = offsetHeight - scrollY - offsetHeight / 500 * 84
      const top = 0 - scrollY / 5
      setScrollOptions({
   
   
        opacity: computerOpacty <= 0 ? 0 : computerOpacty,
        top,
        suctionTop: offsetTop < radio
      })
    }
    document.addEventListener('scroll', handerScroll)
    return function () {
   
   
      document.removeEventListener('scroll', handerScroll)
    }
  }, [])
  return [scrollOptions, dom]
}

具体设计思路

1 我们用一个 useRef来获取需要元素
2 用 useEffect 来初始化绑定/解绑事件
3 用 useState 来保存要改变的状态,通知组件渲染。

中间的计算过程我们可以先不计,最终达到预期效果。

有关性能优化

这里说一下一个无关hooks本身的性能优化点,我们在改变top值的时候 ,尽量用改变transform Y值代替直接改变top值,原因如下

1 transform 是可以让GPU加速的CSS3属性,在性能方便优于直接改变top值。
2 在ios端,固定定位频繁改变top值,会出现闪屏兼容性。

实战二:控制表单状态-useFormChange

背景:但我们遇到例如 列表的表头搜索,表单提交等场景,需要逐一改变每个formItemvalue值,需要逐一绑定事件是比较麻烦的一件事,于是在平时的开发中,我们来用一个hooks来统一管理表单的状态。

1 实现效果

demo效果如下

获取表单

重置表单

2 自定义useFormChange设计思路

需要实现功能

1 控制每一个表单的值。
2 具有表单提交,获取整个表单数据功能。
3 点击重置,重置表单功能。

页面

import useFormChange from '../../hooks/useFormChange'
import './index.less'
const selector = ['嘿嘿', '哈哈', '嘻嘻']
function index() {
    const [formData, setFormItem, reset] = useFormChange()
    const {
        name,
        options,
        select
    } = formData
    return <View className='formbox' >
        <View className='des' >文本框</View>
        <AtInput  name='value1' title='名称'  type='text' placeholder='请输入名称'  value={name} onChange={(value) => setFormItem('name', value)}
        />
        <View className='des' >单选</View>
        <AtRadio
          options={[
                { label: '单选项一', value: 'option1' },
                { label: '单选项二', value: 'option2' },
            ]}
          value={options}
          onClick={(value) => setFormItem('options', value)}
        />
        <View className='des' >下拉框</View>
        <Picker mode='selector' range={selector} onChange={(e) => setFormItem('select',selector[e.detail.value])} >
            <AtList>
                <AtListItem
                  title='当前选择'
                  extraText={select}
                />
            </AtList>
        </Picker>
        <View className='btns' >
            <AtButton type='primary' onClick={() => console.log(formData)} >提交</AtButton>
            <AtButton className='reset' onClick={reset} >重置</AtButton>
        </View>
    </View>
}

useFormChange

  /* 表单/表头搜素hooks */
  function useFormChange() {
    const formData = useRef({})
    const [, forceUpdate] = useState(null)
    const handerForm = useMemo(()=>{
      /* 改变表单单元项 */
      const setFormItem = (keys, value) => {      
        const form = formData.current
        form[keys] = value
        forceUpdate(value)
      }
      /* 重置表单 */
      const resetForm = () => {
        const current = formData.current
        for (let name in current) {
          current[name] = ''
        }
        forceUpdate('')
      }
      return [ setFormItem ,resetForm ]
    },[])

    return [ formData.current ,...handerForm ]
  }

具体流程分析:
1 我们用useRef来缓存整个表单的数据。
2 用useState单独做更新,不需要读取useState状态。
3 声明重置表单方法resetForm , 设置表单单元项change方法,

这里值得一提的问题是 为什么用useRef来缓存formData数据,而不是直接用useState

原因一
我们都知道当用useMemo,useCallbackAPI的时候,如果引用了useState,就要把useState值作为deps传入,否侧由于useMemo,useCallback缓存了useState旧的值,无法得到新得值,但是useRef不同,可以直接读取/改变useRef里面缓存的数据。

原因二
同步useState
useState在一次使用useState改变state值之后,我们是无法获取最新的state,如下demo

function index(){
    const [ number , setNumber ] = useState(0)
    const changeState = ()=>{
        setNumber(number+1)
        console.log(number) //组件更新  -> 打印number为0 -> 并没有获取到最新的值
    }
   return <View>
       <Button onClick={changeState} >点击改变state</Button>
   </View>
}

我们可以用 useRef useState达到同步效果

function index(){
    const number = useRef(0)
    const [  , forceUpdate ] = useState(0)
    const changeState = ()=>{
        number.current++
        forceUpdate(number.current)
        console.log(number.current) //打印值为1,组件更新,值改变
    }
   return <View>
       <Button onClick={changeState} >点击改变state</Button>
   </View>
}

性能优化
useMemo来优化setFormItem ,resetForm方法,避免重复声明,带来的性能开销。

实战三:控制表格/列表-useTableRequset

背景:当我们需要控制带分页,带查询条件的表格/列表的情况下。

1 实现效果

1 统一管理表格的数据,包括列表,页码,总页码数等信息
2 实现切换页码,更新数据。

2 自定义useTableRequset设计思路

1 我们需要state来保存列表数据,总页码数,当前页面等信息。
2 需要暴露一个方法用于,改变分页数据,从新请求数据。

解析来我们看一下具体的实现方案。

页面

function getList(payload){
  const query = formateQuery(payload)
  return fetch('http://127.0.0.1:7001/page/tag/list?'+ query ).then(res => res.json())
}
export default function index(){
    /* 控制表格查询条件 */
    const [ query , setQuery ] = useState({})
    const [tableData, handerChange] = useTableRequest(query,getList)
    const { page ,pageSize,totalCount ,list } = tableData
    return <View className='index' >
        <View className='table' >
            <View className='table_head' >
                <View className='col' >技术名称</View>
                <View className='col' >icon</View>
                <View className='col' >创建时间</View>
            </View>
            <View className='table_body' >
               {
                   list.map(item=><View className='table_row' key={item.id}  >
                        <View className='col' >{ item.name }</View>
                        <View className='col' > <Image className='col col_image'  src={Icons[item.icon].default} /></View>
                        <View className='col' >{ item.createdAt.slice(0,10) }</View>
                   </View>)
               }
            </View>
        </View>
        <AtPagination 
          total={Number(totalCount)} 
          icon
          pageSize={Number(pageSize)}
          onPageChange={(mes)=>handerChange({ page:mes.current })}
          current={Number(page)}
        ></AtPagination>
    </View>
}

useTableRequset

 /* table 数据更新 hooks */
export default function useTableRequset(query, api) {
    /* 是否是第一次请求 */
    const fisrtRequest = useRef(false)
    /* 保存分页信息 */
    const [pageOptions, setPageOptions] = useState({
      page: 1,
      pageSize: 3
    })
    /* 保存表格数据 */
    const [tableData, setTableData] = useState({
      list: [],
      totalCount: 0,
      pageSize: 3,
      page:1,
    })
    /* 请求数据 ,数据处理逻辑根后端协调着来 */
    const getList = useMemo(() => {
      return async payload => {
        if (!api) return
        const data = await api(payload || {...query, ...pageOptions})
        if (data.code == 0) {
          setTableData(data.data)
          fisrtRequest.current = true
        } 
      }
    }, [])
    /* 改变分页,重新请求数据 */
    useEffect(() => {
      fisrtRequest.current && getList({
        ...query,
        ...pageOptions
      })
    }, [pageOptions])
    /* 改变查询条件。重新请求数据 */
    useEffect(() => {
      getList({
        ...query,
        ...pageOptions,
        page: 1
      })
    }, [query])
    /* 处理分页逻辑 */
    const handerChange = useMemo(() => (options) => setPageOptions({...options }), [])

    return [tableData, handerChange, getList]
  }

具体设计思路:

因为是demo项目,我们用本地服务器做了一个数据查询的接口,为的是模拟数据请求。

1 用一个useRef来缓存是否是第一次请求数据。

2 用useState 保存返回的数据和分页信息。

3 用两个useEffect分别处理,对于列表查询条件的更改,或者是分页状态更改,启动副作用钩子,重新请求数据,这里为了区别两种状态更改效果,实际也可以用一个effect来处理。

4 暴露两个方法,分别是请求数据和处理分页逻辑。

性能优化

1 我们用一个useRef来缓存是否是第一次渲染,目的是为了,初始化的时候,两个useEffect钩子都会执行,为了避免重复请求数据。

2 对于请求数据和处理分页逻辑,避免重复声明,我们用useMemo加以优化。

需要注意的是,这里把请求数据后处理逻辑连同自定义hooks封装在一起,在实际项目中,要看和后端约定的数据返回格式来制定属于自己的hooks。

实战四:控制拖拽效果-useDrapDrop

背景:用transformhooks实现了拖拽效果,无需设置定位。

1 实现效果

独立hooks绑定独立的dom元素,使之能实现自由拖拽效果。

2 useDrapDrop具体实现思路

需要实现的功能:

1 通过自定义hooks计算出来的 x ,y 值,通过将transformtranslate属性设置当前计算出来的x,y实现拖拽效果。

2 自定义hooks能抓取当前dom元素容器。

页面

export default function index (){
   const [ style1 , dropRef ]= useDrapDrop()
   const [style2,dropRef2] = useDrapDrop()
   return <View className='index'>
      <View 
        className='drop1' 
        ref={dropRef}
        style={
  
  {transform:`translate(${style1.x}px, ${style1.y}px)`}} 
      >drop1</View>
      <View 
        className='drop2'   
        ref={dropRef2}
        style={
  
  {transform:`translate(${style2.x}px, ${style2.y}px)`}} 
      >drop2</View>
      <View 
        className='drop3'
      >drop3</View>
   </View>
}

注意点:
我们没有用,left,和top来改变定位,css3transform能够避免浏览器的重排和回流,性能优化上要强于直接改变定位的top,left值。
由于我们模拟环境考虑到是h5移动端,所以用 webviewtouchstart , touchmove ,ontouchend 事件来进行模拟。

核心代码-useDrapDrop

/* 移动端 -> 拖拽自定义效果(不使用定位) */
function useDrapDrop() {
  /* 保存上次移动位置 */  
  const lastOffset = useRef({
      x:0, /* 当前x 值 */
      y:0, /* 当前y 值 */
      X:0, /* 上一次保存X值 */
      Y:0, /* 上一次保存Y值 */
  })  
  /* 获取当前的元素实例 */
  const currentDom = useRef(null)
  /* 更新位置 */
  const [, foceUpdate] = useState({})
  /* 监听开始/移动事件 */
  const [ ontouchstart ,ontouchmove ,ontouchend ] = useMemo(()=>{
      /* 保存left right信息 */
      const currentOffset = {} 
      /* 开始滑动 */
      const touchstart = function (e) {   
        const targetTouche = e.targetTouches[0]
        currentOffset.X = targetTouche.clientX
        currentOffset.Y = targetTouche.clientY
      }
      /* 滑动中 */
      const touchmove = function (e){
        const targetT = e.targetTouches[0]
        let x =lastOffset.current.X  + targetT.clientX - currentOffset.X
        let y =lastOffset.current.Y  + targetT.clientY - currentOffset.Y     
        lastOffset.current.x = x
        lastOffset.current.y = y
        foceUpdate({
           x,y
        })
      }
      /* 监听滑动停止事件 */
      const touchend =  () => {
        lastOffset.current.X = lastOffset.current.x
        lastOffset.current.Y = lastOffset.current.y
      }
      return [ touchstart , touchmove ,touchend]
  },[])
  useLayoutEffect(()=>{
    const dom = currentDom.current
    dom.ontouchstart = ontouchstart
    dom.ontouchmove = ontouchmove
    dom.ontouchend = ontouchend
  },[])
  return [ { x:lastOffset.current.x,y:lastOffset.current.y } , currentDom]
}

具体设计思路:

1 对于拖拽效果,我们需要实时获取dom元素的位置信息,所以我们需要一个useRef来抓取dom元素。

2 由于我们用的是transfrom改变位置,所以需要保存一下当前位置和上一次transform的位置,所以我们用一个useRef来缓存位置。

3 我们通过useRef改变x,y值,但是需要渲染新的位置,所以我们用一个useState来专门产生组件更新。

4 初始化的时候我们需要给当前的元素绑定事件,因为在初始化的时候我们可能精确需要元素的位置信息,所以我们用useLayoutEffect钩子来绑定touchstart , touchmove ,ontouchend等事件。

总结

以上就是我在react自定义hooks上的总结,和一些实际的应用场景,我们项目中,80%的表单列表场景,都可以用上述hooks来解决。

纸上得来终觉浅,绝知此事要躬行,真正玩好,玩转hooks,是一个日积月累的过程,怎么去设计一个符合业务场景的hooks,需要我们不断的实战,不断的总结。

最后大家觉得还不错的话,就 点赞 + 关注 一波,持续分享技术文章。

公众号:前端Sharing

相关文章
|
20天前
|
设计模式 算法 开发者
探索编程语言中的设计模式:从理论到实践
设计模式,这一编程世界中的灯塔,为无数开发者照亮了复杂问题解决的道路。本文将深入探讨设计模式在编程实践中的运用,以代码示例揭示其背后的智慧。无论你是初学者还是资深开发者,都能在这里找到启发和共鸣。让我们一起领略设计模式的魅力,开启编程世界的新篇章!
|
28天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
1月前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
23 3
|
1月前
|
设计模式 监控 算法
Python编程中的设计模式应用与实践感悟###
在Python这片广阔的编程疆域中,设计模式如同导航的灯塔,指引着开发者穿越复杂性的迷雾,构建出既高效又易于维护的代码结构。本文基于个人实践经验,深入探讨了几种核心设计模式在Python项目中的应用策略与实现细节,旨在为读者揭示这些模式背后的思想如何转化为提升软件质量的实际力量。通过具体案例分析,展现了设计模式在解决实际问题中的独特魅力,鼓励开发者在日常编码中积极采纳并灵活运用这些宝贵的经验总结。 ###
|
1月前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
22天前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践感悟####
本文作为一篇技术性文章,旨在深入探讨Python编程中设计模式的应用价值与实践心得。在快速迭代的软件开发领域,设计模式如同导航灯塔,指引开发者构建高效、可维护的软件架构。本文将通过具体案例,展现设计模式如何在实际项目中解决复杂问题,提升代码质量,并分享个人在实践过程中的体会与感悟。 ####
|
1月前
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
35 2
|
25天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
23 0
|
25天前
|
前端开发
React Hooks:从基础到进阶的深入理解
React Hooks:从基础到进阶的深入理解
30 0