10分鐘教你手寫8個經常使用的自定義hooks

前言

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。本文是一篇以實戰爲主的文章,主要講解實際項目中如何使用hooks以及一些最佳實踐,不會一步步再介紹一遍react hooks的由來和基本使用,由於寫hooks的文章不少,並且官網對於react hooks的介紹也很詳細,因此你們不熟悉的能夠看一遍官網javascript

你將收穫

  • react hooks核心API使用以及注意事項
  • 實現一個小型redux
  • 實現自定義的useState
  • 實現自定義的useDebounce
  • 實現自定義的useThrottle
  • 實現自定義useTitle
  • 實現自定義的useUpdate
  • 實現自定義的useScroll
  • 實現自定義的useMouse
  • 實現自定義的createBreakpoint

正文

1. react hooks核心API使用注意事項

筆者在項目中經常使用的hooks主要有useState, useEffect,useCallback,useMemo,useRef。固然像useReducer, useContext, createContext這些鉤子在H5遊戲中也會使用,由於不須要維護錯綜複雜的狀態,因此咱們徹底能夠由上述三個api構建一個本身的小型redux(後面會介紹如何實現小型的redux)來處理全局狀態,可是對於企業複雜項目來講,咱們使用redux及其生態會更加高效一些。css

咱們在使用hooks和函數組件編寫咱們的組件時,第一個要考慮的就是渲染性能,咱們知道若是在不作任何處理時,咱們在函數組件中使用setState都會致使組件內部從新渲染,一個比較典型的場景: html

當咱們在容器組件手動更新了任何state時,容器內部的各個子組件都會從新渲染,爲了不這種狀況出現,咱們通常都會使用memo將函數組件包裹,來達到class組件的pureComponent的效果:

import React, { memo, useState, useEffect } from 'react'
const A = (props) => {
  console.log('A1')
  useEffect(() => {
    console.log('A2')
  })
  return <div>A</div>
}

const B = memo((props) => {
  console.log('B1')
  useEffect(() => {
    console.log('B2')
  })
  return <div>B</div>
})

const Home = (props) => {
  const [a, setA] = useState(0)
  useEffect(() => {
    console.log('start')
    setA(1)
  }, [])
  return <div><A n={a} /><B /></div> } 複製代碼

當咱們將B用memo包裹後,狀態a的更新將不會致使B組件從新渲染。其實僅僅優化這一點還遠遠不夠的,好比說咱們子組件用到了容器組件的某個變量或者函數,那麼當容器內部的state更新以後,這些變量和函數都會從新賦值,這樣就會致使即便子組件使用了memo包裹也仍是會從新渲染,那麼這個時候咱們就須要使用useMemouseCallback了。前端

useMemo能夠幫咱們將變量緩存起來,useCallback能夠緩存回調函數,它們的第二個參數和useEffect同樣,是一個依賴項數組,經過配置依賴項數組來決定是否更新。vue

import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  useEffect(() => {
    setA(1)
  }, [])

  const add = useCallback(() => {
    console.log('b', b)
  }, [b])

  const name = useMemo(() => {
    return b + 'xuxi'
  }, [b])
  return <div><A n={a} /><B add={add} name={name} /></div>
}
複製代碼

此時a更新後B組件不會再從新渲染。以上幾個優化步驟主要是用來優化組件的渲染性能,咱們平時還會涉及到獲取組件dom和使用內部閉包變量的情景,這個時候咱們就可使用useRefjava

useRef返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。node

function AutoFocusIpt() {
  const inputEl = useRef(null);
  const useEffect(() => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  }, []);
  return (
    <>
      <input ref={inputEl} type="text" />
    </>
  );
}
複製代碼

除了以上應用場景外,咱們還能夠利用它來實現class組件的setState的功能,具體實現後面會有介紹。react

2. 實現一個小型redux

實現redux咱們會利用以前說的useReducer, useContext, createContext這三個api,至於如何實現redux,其實網上也有不少實現方式,這裏筆者寫一個demo供你們參考:webpack

// actionType.js
const actionType = {
  INSREMENT: 'INSREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET'
}
export default actionType

// actions.js
import actionType from './actionType'
const add = (num) => ({
    type: actionType.INSREMENT,
    payload: num
})

const dec = (num) => ({
    type: actionType.DECREMENT,
    payload: num
})

const getList = (data) => ({
    type: actionType.GETLIST,
    payload: data
})
export {
    add,
    dec,
    getList
}

// reducer.js
function init(initialCount) {
  return {
    count: initialCount,
    total: 10,
    user: {},
    article: []
  }
}

function reducer(state, action) {
  switch (action.type) {
    case actionType.INSREMENT:
      return {count: state.count + action.payload};
    case actionType.DECREMENT:
      return {count: state.count - action.payload};
    case actionType.RESET:
      return init(action.payload);
    default:
      throw new Error();
  }
}

export { init, reducer }

// redux.js
import React, { useReducer, useContext, createContext } from 'react'
import { init, reducer } from './reducer'

const Context = createContext()
const Provider = (props) => {
  const [state, dispatch] = useReducer(reducer, props.initialState || 0, init);
    return (
      <Context.Provider value={{state, dispatch}}> { props.children } </Context.Provider> ) } export { Context, Provider } 複製代碼

其實還有更優雅的方式實現,筆者以前也寫了幾套redux模版,歡迎一塊兒討論哈。接下來咱們進入正文,來帶你們實現幾個經常使用的自定義hooks。css3

3. 實現自定義的useState,支持相似class組件setState方法

熟悉react的朋友都知道,咱們使用class組件更新狀態時,setState會支持兩個參數,一個是更新後的state或者回調式更新的state,另外一個參數是更新後的回調函數,以下面的用法:

this.setState({num: 1}, () => {
    console.log('updated')
})
複製代碼

可是hooks函數的useState第二個參數回調支持相似class組件的setState的第一個參數的用法,並不支持第二個參數回調,可是不少業務場景中咱們又但願hooks組件能支持更新後的回調這一方法,那該怎麼辦呢?其實問題也很簡單,咱們只要對hooks原理和api很是清楚的話,就能夠經過自定義hooks來實現,這裏咱們藉助上面提到的useRef和useEffect配合useState來實現這一功能。

注:react hooks的useState必定要放到函數組件的最頂層,不能寫在ifelse等條件語句當中,來確保hooks的執行順序一致,由於useState底層採用鏈表結構實現,有嚴格的順序之分。

咱們先來看看實現的代碼:

import { useEffect, useRef, useState } from 'react'

const useXState = (initState) => {
    const [state, setState] = useState(initState)
    let isUpdate = useRef()
    const setXState = (state, cb) => {
      setState(prev => {
        isUpdate.current = cb
        return typeof state === 'function' ? state(prev) : state
      })
    }
    useEffect(() => {
      if(isUpdate.current) {
        isUpdate.current()
      }
    })
  
    return [state, setXState]
  }

export default useXState
複製代碼

筆者利用useRef的特性來做爲標識區分是掛載仍是更新,當執行setXstate時,會傳入和setState如出一轍的參數,而且將回調賦值給useRef的current屬性,這樣在更新完成時,咱們手動調用current便可實現更新後的回調這一功能,是否是很巧妙呢?

4. 實現自定義的useDebounce

節流函數和防抖函數想必你們也不陌生,爲了讓咱們在開發中更優雅的使用節流和防抖函數,咱們每每須要讓某個state也具備節流防抖的功能,或者某個函數的調用,爲了不頻繁調用,咱們每每也會採起節截流防抖這一思想,原生的節流防抖函數可能如一下代碼所示:

// 節流
function throttle(func, ms) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > ms) {
            func.apply(context, args);
            previous = now;
        }
    }
}

// 防抖
function debounce(func, ms) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, ms);
    }
}
複製代碼

那麼咱們首先來實現一下防抖的hooks,代碼以下:

import { useEffect, useRef } from 'react'

const useDebounce = (fn, ms = 30, deps = []) => {
    let timeout = useRef()
    useEffect(() => {
        if (timeout.current) clearTimeout(timeout.current)
        timeout.current = setTimeout(() => {
            fn()
        }, ms)
    }, deps)

    const cancel = () => {
        clearTimeout(timeout.current)
        timeout = null
    }
  
    return [cancel]
  }

export default useDebounce
複製代碼

由代碼能夠知道,useDebounce接受三個參數,分別爲回調函數,時間間隔以及依賴項數組,它暴露了cancel API,主要是用來控制什麼時候中止防抖函數用的。具體使用以下:

// ...
import { useDebounce } from 'hooks'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  const [cancel] = useDebounce(() => {
    setB(a)
  }, 2000, [a])

  const changeIpt = (e) => {
    setA(e.target.value)
  }
  return <div> <input type="text" onChange={changeIpt} /> { b } { a } </div> } 複製代碼

以上代碼就實現了state的debounce的功能,具體效果以下圖所示:

5. 實現自定義的useThrottle

同理,咱們繼續來實現節流的hooks函數。直接上代碼:

import { useEffect, useRef, useState } from 'react'

const useThrottle = (fn, ms = 30, deps = []) => {
    let previous = useRef(0)
    let [time, setTime] = useState(ms)
    useEffect(() => {
        let now = Date.now();
        if (now - previous.current > time) {
            fn();
            previous.current = now;
        }
    }, deps)

    const cancel = () => {
        setTime(0)
    }
  
    return [cancel]
  }

export default useThrottle
複製代碼

代碼和自定義useDebounce相似,但須要注意一點就是爲了實現cancel功能,咱們使用了內部state來處理,經過控制時間間隔來取消節流效果,固然還有不少其餘方法能夠實現這個hooks API。具體效果以下:

6. 實現自定義useTitle

自定義的useTitle hooks其實使用場景也不少,由於咱們目前大部分項目都是採用SPA或者混合SPA的方式開發,對於不一樣的路由咱們一樣但願想多頁應用同樣能切換到對應的標題,這樣可讓用戶更好的知道頁面的主題和內容。這個hooks的實現也很簡單,咱們直接上代碼:

import { useEffect } from 'react'

const useTitle = (title) => {
    useEffect(() => {
      document.title = title
    }, [])
  
    return
  }

export default useTitle
複製代碼

以上代碼能夠看出咱們只須要在useEffect中設置document的title屬性就行了,咱們不須要return任何值。其實還有更優雅和複雜的實現方法,這裏就不一一舉例了。具體使用以下:

const Home = () => {
    // ...
    useTitle('趣談前端')
    
    return <div>home</div>
}
複製代碼

7. 實現自定義的useUpdate

咱們都知道若是想讓組件從新渲染,咱們不得不更新state,可是有時候業務須要的state是不必更新的,咱們不能僅僅爲了讓組件會從新渲染而強制讓一個state作無心義的更新,因此這個時候咱們就能夠自定義一個更新的hooks來優雅的實現組件的強制更新,實現代碼以下:

import { useState } from 'react'

const useUpdate = () => {
    const [, setFlag] = useState()
    const update = () => {
        setFlag(Date.now())
    }
  
    return update
  }

export default useUpdate
複製代碼

以上代碼能夠發現,咱們useUpdate鉤子返回了一個函數,該函數就是用來強制更新用的。使用方法以下:

const Home = (props) => {
  // ...
  const update = useUpdate()
  return <div> {Date.now()} <div><button onClick={update}>update</button></div> </div>
}
複製代碼

效果以下:

8. 實現自定義的useScroll

自定義的useScroll也是高頻出現的問題之一,咱們每每會監聽一個元素滾動位置的變化來決定展示那些內容,這個應用場景在H5遊戲開發中應用十分普遍,接下來咱們來看看實現代碼:

import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0,0])

  useEffect(() => {
    function handleScroll(e){
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll, false)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll, false)
    }
  }, [])
  
  return pos
}

export default useScroll
複製代碼

由以上代碼可知,咱們在鉤子函數裏須要傳入一個元素的引用,這個咱們能夠在函數組件中採用ref和useRef來獲取到,鉤子返回了滾動的x,y值,即滾動的左位移和頂部位移,具體使用以下:

import React, { useRef } from 'react'

 import { useScroll } from 'hooks'
const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return <div> <div ref={scrollRef}> <div className="innerBox"></div> </div> <div>{ x }, { y }</div> </div>
}
複製代碼

經過使用useScroll,鉤子將會幫咱們自動監聽容器滾動條的變化從而實時獲取滾動的位置。具體效果以下:

9. 實現自定義的useMouse和實現自定義的createBreakpoint

自定義的useMouse和createBreakpoint的實現方法和useScroll相似,都是監聽窗口或者dom的事件來自動更新咱們須要的值,這裏我就不一一實現了,若是不懂的能夠和我交流。經過這些自定義鉤子能大大提升咱們代碼的開發效率,並將重複代碼進行有效複用,因此你們在工做中能夠多嘗試。

當咱們寫了不少自定鉤子時,一個好的開發經驗就是統一管理和分發這些鉤子,筆者建議能夠在項目中單獨建一個hooks的目錄專門存放這些可複用的鉤子,方便管理和維護。以下:

最後

若是想獲取本次項目完整的源碼, 或者想學習更多H5遊戲, webpacknodegulpcss3javascriptnodeJScanvas數據可視化等前端知識和實戰,歡迎在公號《趣談前端》加入咱們的技術羣一塊兒學習討論,共同探索前端的邊界。

更多推薦

相關文章
相關標籤/搜索