玩轉react-hooks,自定義hooks設計模式及其實戰

前言

自從react16.8,react-hooks誕生以來,在工做中一直使用hooks,一年多的時間裏,接觸的react項目,漸漸使用function無狀態組件代替了classs聲明的有狀態組件,期間也總結了一些心得。尤爲對於近期三個月的項目裏,一點點用自定義hooks來處理公司項目中重複邏輯,整體感受還不錯。今天給你們講講我在工做中對react-hooks心得,和一些自定義hooks的設計思想,把在工做中的經驗分享給你們。css

自定義hooks設計

又回到那個問題?什麼是hooks。

react-hooks是react16.8之後,react新增的鉤子API,目的是增長代碼的可複用性,邏輯性,彌補無狀態組件沒有生命週期,沒有數據管理狀態state的缺陷。筆者認爲,react-hooks思想和初衷,也是把組件,顆粒化,單元化,造成獨立的渲染環境,減小渲染次數,優化性能。前端

還不明白react-hooks的夥伴能夠看的另一篇文章: react-hooks如何使用?react

什麼是自定義hooks

自定義hooks是在react-hooks基礎上的一個拓展,能夠根據業務須要制定知足業務須要的hooks,更注重的是邏輯單元。經過業務場景不一樣,咱們到底須要react-hooks作什麼,怎麼樣把一段邏輯封裝起來,作到複用,這是自定義hooks產生的初衷。ios

如何設計一個自定義hooks,設計規範

邏輯+ 組件

hooks 專一的就是邏輯複用, 是咱們的項目,不只僅停留在組件複用的層面上。hooks讓咱們能夠將一段通用的邏輯存封起來。將咱們須要它的時候,開箱即用便可。css3

自定義hooks-驅動條件

hooks本質上是一個函數。函數的執行,決定與無狀態組件組件自身的執行上下文。每次函數的執行(本質上就是組件的更新)就會執行自定義hooks的執行,因而可知組件自己執行和hooks的執行一模一樣。git

那麼prop的修改,useState,useReducer使用是無狀態組件更新條件,那麼就是驅動hooks執行的條件。 咱們用一幅圖來表示如上關係。github

自定義hooks-通用模式

咱們設計的自定義react-hooks應該是長的這樣的。web

const [ xxx , ... ] = useXXX(參數A,參數B...)
複製代碼

在咱們在編寫自定義hooks的時候,要特別~特別~特別關注的是傳進去什麼返回什麼。 返回的東西是咱們真正須要的。更像一個工廠,把原材料加工,最後返回咱們。正以下圖所示json

自定義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

相關文章
相關標籤/搜索