React 全部 Hooks 使用大全 (包含 React v18 版本 )

一 前言

React hooks 是 react16.8 以後,react 新增的鉤子 API,目的是增加代碼的可複用性,邏輯性,彌補無狀態組件沒有生命週期,沒有數據管理狀態 state 的缺陷。本章節筆者將介紹目前 React 提供的所有 hooks ,介紹其功能類型和基本使用方法。

創作不易,希望屏幕前的你能給筆者賞個,以此鼓勵我繼續創作前端硬文。🌹🌹🌹

1.1 技術背景

react hooks 解決了什麼問題?

先設想一下,如果沒有 Hooks,函數組件能夠做的只是接受 Props、渲染 UI ,以及觸發父組件傳過來的事件。所有的處理邏輯都要在類組件中寫,這樣會使 class 類組件內部錯綜複雜,每一個類組件都有一套獨特的狀態,相互之間不能複用,即便是 React 之前出現過 mixin 等複用方式,但是伴隨出 mixin 模式下隱式依賴,代碼衝突覆蓋等問題,也不能成爲 React 的中流砥柱的邏輯複用方案。所以 React 放棄 mixin 這種方式。

類組件是一種面向對象思想的體現,類組件之間的狀態會隨着功能增強而變得越來越臃腫,代碼維護成本也比較高,而且不利於後期 tree shaking。所以有必要做出一套函數組件代替類組件的方案,於是 Hooks 也就理所當然的誕生了。

所以 Hooks 出現本質上原因是:

爲什麼要使用自定義 Hooks ?

自定義 hooks 是在 React Hooks 基礎上的一個拓展,可以根據業務需求制定滿足業務需要的組合 hooks ,更注重的是邏輯單元。通過業務場景不同,到底需要 React Hooks 做什麼,怎麼樣把一段邏輯封裝起來,做到複用,這是自定義 hooks 產生的初衷。

自定義 hooks 也可以說是 React Hooks 聚合產物,其內部有一個或者多個 React Hooks 組成,用於解決一些複雜邏輯。

1.2 技術願景

目前 hooks 已經成爲 React 主流的開發手段,React 生態也日益朝着 hooks 方向發展,比如 React Router, React Redux 等, hooks 也更契合 React 生態庫的應用。

隨着項目功能模塊越來越複雜,一些公共邏輯不能有效的複用,這些邏輯需要和業務代碼強關聯到一起,這樣會讓整體工程臃腫,功能不能複用,如果涉及到修改邏輯,那麼有可能牽一髮動全身。

所以有必要使用自定義 hooks 的方式,hooks 可以把重複的邏輯抽離出去,根據需要創建和業務功能綁定的業務型 hooks ,或者是根據具體功能創建的功能型 hooks 。

1.3 功能概覽

在 React 的世界中,不同的 hooks 使命也是不同的,我這裏對 React hooks 按照功能分類,分成了 數據更新驅動狀態獲取與傳遞,執行副作用,狀態派生與保存,和工具類型, 具體功能劃分和使用場景如下:

WechatIMG32.png

二 hooks 之數據更新驅動

2.1 useState

useState 可以使函數組件像類組件一樣擁有 state,函數組件通過 useState 可以讓組件重新渲染,更新視圖。

useState 基礎介紹:

const [ ①state , ②dispatch ] = useState(③initData)

① state,目的提供給 UI ,作爲渲染視圖的數據源。

② dispatchAction 改變 state 的函數,可以理解爲推動函數組件渲染的渲染函數。

③ initData 有兩種情況,第一種情況是非函數,將作爲 state 初始化的值。第二種情況是函數,函數的返回值作爲 useState 初始化的值。

useState 基礎用法:

const DemoState = (props) ={
   /* number爲此時state讀取值 ,setNumber爲派發更新的函數 */
   let [number, setNumber] = useState(0) /* 0爲初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()={
         setNumber(number+1)
         console.log(number) /* 這裏的number是不能夠即使改變的  */
       } } ></button>
   </div>)
}

useState 注意事項:

① 在函數組件一次執行上下文中,state 的值是固定不變的。

function Index(){
    const [ number, setNumber ] = React.useState(0)
    const handleClick = () => setInterval(()=>{
        // 此時 number 一直都是 0
        setNumber(number + 1 ) 
    },1000)
    return <button onClick={ handleClick } > 點擊 { number }</button>
}

② 如果兩次 dispatchAction 傳入相同的 state 值,那麼組件就不會更新。

export default function Index(){
    const [ state  , dispatchState ] = useState({ name:'alien' })
    const  handleClick = ()=>{ // 點擊按鈕,視圖沒有更新。
        state.name = 'Alien'
        dispatchState(state) // 直接改變 `state`,在內存中指向的地址相同。
    }
    return <div>
         <span> { state.name }</span>
        <button onClick={ handleClick }  >changeName++</button>
    </div>
}

③ 當觸發 dispatchAction 在當前執行上下文中獲取不到最新的 state, 只有再下一次組件 rerender 中才能獲取到。

2.2 useReducer

useReducer 是 react-hooks 提供的能夠在無狀態組件中運行的類似 redux 的功能 api 。

useReducer 基礎介紹:

const [ ①state , ②dispatch ] = useReducer(③reducer)

① 更新之後的 state 值。

② 派發更新的 dispatchAction 函數, 本質上和 useState 的 dispatchAction 是一樣的。

③ 一個函數 reducer ,我們可以認爲它就是一個 redux 中的 reducer , reducer 的參數就是常規 reducer 裏面的 state 和 action, 返回改變後的 state, 這裏有一個需要注意的點就是:如果返回的 state 和之前的 state ,內存指向相同,那麼組件將不會更新。

useReducer 基礎用法:

const DemoUseReducer = ()=>{
    /* number爲更新後的state值,  dispatchNumbner 爲當前的派發函數 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值爲新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      當前值:{ number }
      { /* 派發更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >減少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >賦值</button>
      { /* 把dispatch 和 state 傳遞給子組件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

2.3 useSyncExternalStore

useSyncExternalStore 的誕生並非偶然,和 v18 的更新模式下外部數據的 tearing 有着十分緊密的關聯。useSyncExternalStore 能夠讓 React 組件在 concurrent 模式下安全地有效地讀取外接數據源,在組件渲染過程中能夠檢測到變化,並且在數據源發生變化的時候,能夠調度更新。當讀取到外部狀態發生了變化,會觸發一個強制更新,來保證結果的一致性。

useSyncExternalStore 基礎介紹:

useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
)

① subscribe 爲訂閱函數,當數據改變的時候,會觸發 subscribe,在 useSyncExternalStore 會通過帶有記憶性的 getSnapshot 來判別數據是否發生變化,如果發生變化,那麼會強制更新數據。

② getSnapshot 可以理解成一個帶有記憶功能的選擇器。當 store 變化的時候,會通過 getSnapshot 生成新的狀態值,這個狀態值可提供給組件作爲數據源使用,getSnapshot 可以檢查訂閱的值是否改變,改變的話那麼會觸發更新。

③ getServerSnapshot 用於 hydration 模式下的 getSnapshot。

useSyncExternalStore 基礎用法:

import { combineReducers , createStore  } from 'redux'

/* number Reducer */
function numberReducer(state=1,action){
    switch (action.type){
      case 'ADD':
        return state + 1
      case 'DEL':
        return state - 1
      default:
        return state
    }
}

/* 註冊reducer */
const rootReducer = combineReducers({ number:numberReducer  })
/* 創建 store */
const store = createStore(rootReducer,{ number:1  })

function Index(){
    /* 訂閱外部數據源 */
    const state = useSyncExternalStore(store.subscribe,() => store.getState().number)
    console.log(state)
    return <div>
        {state}
        <button onClick={() => store.dispatch({ type:'ADD' })} >點擊</button>
    </div>
}

點擊按鈕,會觸發 reducer ,然後會觸發 store.subscribe 訂閱函數,執行 getSnapshot 得到新的 number ,判斷 number 是否發生變化,如果變化,觸發更新。

2.4 useTransition

在 React v18 中,有一種新概念叫做過渡任務,這種任務是對比立即更新任務而產生的,通常一些影響用戶交互直觀響應的任務,例如按鍵,點擊,輸入等,這些任務需要視圖上立即響應,所以可以稱之爲立即更新的任務,但是有一些更新不是那麼急迫,比如頁面從一個狀態過渡到另外一個狀態,這些任務就叫做過渡任務。打個比方如下圖當點擊 tab 從 tab1 切換到 tab2 的時候,本質上產生了兩個更新任務。

這兩個任務,用戶肯定希望 hover 狀態的響應更迅速,而內容的響應有可能還需要請求數據等操作,所以更新狀態並不是立馬生效,通常還會有一些 loading 效果。所以第一個任務作爲立即執行任務,而第二個任務就可以視爲過渡任務

WechatIMG6496.jpeg

useTransition 基礎介紹:

useTransition 執行返回一個數組。數組有兩個狀態值:

import { useTransition } from 'react' 
/* 使用 */
const  [ isPending , startTransition ] = useTransition ()

useTransition 基礎用法:

除了上述切換 tab 場景外,還有很多場景非常適合 useTransition 產生的過渡任務,比如輸入內容,實時搜索並展示數據,這本質上也是有兩個優先級的任務,第一個任務就是受控表單的實時響應;第二個就是輸入內容改變, 數據展示的變化。那麼接下來我們寫一個 demo 來看一下 useTransition 的基本使用。

/* 模擬數據 */
const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )

const tab = {
  tab1: mockList1,
  tab2: mockList2,
  tab3: mockList3
}

export default function Index(){
  const [ active, setActive ] = React.useState('tab1') //需要立即響應的任務,立即更新任務
  const [ renderData, setRenderData ] = React.useState(tab[active]) //不需要立即響應的任務,過渡任務
  const [ isPending,startTransition  ] = React.useTransition() 
  const handleChangeTab = (activeItem) ={
     setActive(activeItem) // 立即更新
     startTransition(()=>{ // startTransition 裏面的任務優先級低
       setRenderData(tab[activeItem])
     })
  }
  return <div>
    <div className='tab' >
       { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
    </div>
    <ul className='content' >
       { isPending && <div> loading... </div> }
       { renderData.map(item=> <li key={item} >{item}</li>) }
    </ul>
  </div>
}

如上當切換 tab 的時候,產生了兩個優先級任務,第一個任務是 setActive 控制 tab active 狀態的改變,第二個任務爲 setRenderData 控制渲染的長列表數據 (在現實場景下長列表可能是一些數據量大的可視化圖表)。

2.5 useDeferredValue

React 18 提供了 useDeferredValue 可以讓狀態滯後派生。useDeferredValue 的實現效果也類似於 transtion,當迫切的任務執行後,再得到新的狀態,而這個新的狀態就稱之爲 DeferredValue。

useDeferredValue 基礎介紹:

useDeferredValue 和上述 useTransition 本質上有什麼異同呢?

相同點: useDeferredValue 本質上和內部實現與 useTransition 一樣都是標記成了過渡更新任務。

不同點: useTransition 是把 startTransition 內部的更新任務變成了過渡任務 transtion, 而 useDeferredValue 是把原值通過過渡任務得到新的值,這個值作爲延時狀態。一個是處理一段邏輯,另一個是生產一個新的狀態。

useDeferredValue 接受一個參數 value ,一般爲可變的 state , 返回一個延時狀態 deferrredValue。

const deferrredValue = React.useDeferredValue(value)

useDeferredValue 基礎用法:

接下來把上邊那個例子用 useDeferredValue 來實現。

export default function Index(){
  const [ active, setActive ] = React.useState('tab1') //需要立即響應的任務,立即更新任務
  const deferActive = React.useDeferredValue(active) // 把狀態延時更新,類似於過渡任務
  const handleChangeTab = (activeItem) ={
     setActive(activeItem) // 立即更新
  }
  const renderData = tab[deferActive] // 使用滯後狀態
  return <div>
    <div className='tab' >
       { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
    </div>
    <ul className='content' >
       { renderData.map(item=> <li key={item} >{item}</li>) }
    </ul>
  </div>
  }

如上 active 爲正常改變的狀態,deferActive 爲滯後的 active 狀態,我們使用正常狀態去改變 tab 的 active 狀態,使用滯後的狀態去更新視圖,同樣達到了提升用戶體驗的作用。

三 hooks 之執行副作用

3.1 useEffect

React hooks 也提供了 api ,用於彌補函數組件沒有生命週期的缺陷。其本質主要是運用了 hooks 裏面的 useEffect , useLayoutEffect,還有 useInsertionEffect。其中最常用的就是 useEffect 。我們首先來看一下 useEffect 的使用。

useEffect 基礎介紹:

useEffect(()=>{
    return destory
},dep)

useEffect 第一個參數 callback, 返回的 destory , destory 作爲下一次 callback 執行之前調用,用於清除上一次 callback 產生的副作用。

第二個參數作爲依賴項,是一個數組,可以有多個依賴項,依賴項改變,執行上一次 callback 返回的 destory ,和執行新的 effect 第一個參數 callback 。

對於 useEffect 執行, React 處理邏輯是採用異步調用 ,對於每一個 effect 的 callback, React 會向 setTimeout 回調函數一樣,放入任務隊列,等到主線程任務完成,DOM 更新,js 執行完成,視圖繪製完畢,才執行。所以 effect 回調函數不會阻塞瀏覽器繪製視圖。

useEffect 基礎用法:

/* 模擬數據交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{ 
           resolve({
               name:a,
               age:16,
           }) 
        },500)
    })
}

const Demo = ({ a }) ={
    const [ userMessage , setUserMessage ] :any= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    /* 模擬事件監聽處理函數 */
    const handleResize =()=>{}
    /* useEffect使用 ,這裏如果不加限制 ,會是函數重複執行,陷入死循環*/
    useEffect(()=>{
       /* 請求數據 */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
       /* 定時器 延時器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 操作dom  */
       console.log(div.current) /* div */
       /* 事件監聽等 */
       window.addEventListener('resize', handleResize)
         /* 此函數用於清除副作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    /* 只有當props->a和state->number改變的時候 ,useEffect副作用函數重新執行 ,如果此時數組爲空[],證明函數只有在初始化的時候執行一次相當於componentDidMount */
    },[ a ,number ])
    return (<div ref={div} >
        <span>{ userMessage.name }</span>
        <span>{ userMessage.age }</span>
        <div onClick={ ()=> setNumber(1) } >{ number }</div>
    </div>)
}

如上在 useEffect 中做的功能如下:

3.2 useLayoutEffect

useLayoutEffect 基礎介紹:

useLayoutEffect 和 useEffect 不同的地方是採用了同步執行,那麼和 useEffect 有什麼區別呢?

① 首先 useLayoutEffect 是在 DOM 更新之後,瀏覽器繪製之前,這樣可以方便修改 DOM,獲取 DOM 信息,這樣瀏覽器只會繪製一次,如果修改 DOM 佈局放在 useEffect ,那 useEffect 執行是在瀏覽器繪製視圖之後,接下來又改 DOM ,就可能會導致瀏覽器再次迴流和重繪。而且由於兩次繪製,視圖上可能會造成閃現突兀的效果。

② useLayoutEffect callback 中代碼執行會阻塞瀏覽器繪製。

useEffect 基礎用法:

const DemoUseLayoutEffect = () ={
    const target = useRef()
    useLayoutEffect(() ={
        /*我們需要在dom繪製之前,移動dom到制定位置*/
        const { x ,y } = getPositon() /* 獲取要移動的 x,y座標 */
        animate(target.current,{ x,y })
    }[]);
    return (
        <div >
            <span ref={ target } class></span>
        </div>
    )
}

3.3 useInsertionEffect

useInsertionEffect 基礎介紹:

useInsertionEffect 是在 React v18 新添加的 hooks ,它的用法和 useEffect 和 useLayoutEffect 一樣。那麼這個 hooks 用於什麼呢?

在介紹 useInsertionEffect 用途之前,先看一下 useInsertionEffect 的執行時機。

React.useEffect(()=>{
    console.log('useEffect 執行')
},[])

React.useLayoutEffect(()=>{
    console.log('useLayoutEffect 執行')
},[])

React.useInsertionEffect(()=>{
    console.log('useInsertionEffect 執行')
},[])

打印:useInsertionEffect 執行 -> useLayoutEffect 執行 -> useEffect 執行

可以看到 useInsertionEffect 的執行時機要比 useLayoutEffect 提前,useLayoutEffect 執行的時候 DOM 已經更新了,但是在 useInsertionEffect 的執行的時候,DOM 還沒有更新。本質上 useInsertionEffect 主要是解決 CSS-in-JS 在渲染中注入樣式的性能問題。這個 hooks 主要是應用於這個場景,在其他場景下 React 不期望用這個 hooks 。

useInsertionEffect 模擬使用:

export default function Index(){

  React.useInsertionEffect(()=>{
     /* 動態創建 style 標籤插入到 head 中 */
     const style = document.createElement('style')
     style.innerHTML = `
       .css-in-js{
         color: red;
         font-size: 20px;
       }
     `
     document.head.appendChild(style)
  },[])

  return <div class > hello , useInsertionEffect </div>
}

如上模擬了 useInsertionEffect 的使用。

四 hooks 之狀態獲取與傳遞

4.1 useContext

useContext 基礎介紹

可以使用 useContext ,來獲取父級組件傳遞過來的 context 值,這個當前值就是最近的父級組件 Provider 設置的 value 值,useContext 參數一般是由 createContext 方式創建的 , 也可以父級上下文 context 傳遞的 (參數爲 context)。useContext 可以代替 context.Consumer 來獲取 Provider 中保存的 value 值。

const contextValue = useContext(context)

useContext 接受一個參數,一般都是 context 對象,返回值爲 context 對象內部保存的 value 值。

useContext 基礎用法:

/* 用useContext方式 */
const DemoContext = ()={
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

4.2 useRef

useRef 基礎介紹:

useRef 可以用來獲取元素,緩存狀態,接受一個狀態 initState 作爲初始值,返回一個 ref 對象 cur, cur 上有一個 current 屬性就是 ref 對象需要獲取的內容。

const cur = React.useRef(initState)
console.log(cur.current)

useRef 基礎用法:

useRef 獲取 DOM 元素,在 React Native 中雖然沒有 DOM 元素,但是也能夠獲取組件的節點信息( fiber 信息 )。

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表單組件</div>  dom 節點 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 標記當前dom節點 */}
        <div ref={dom} >表單組件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}

如上通過 useRef 來獲取 DOM 節點。

useRef 保存狀態, 可以利用 useRef 返回的 ref 對象來保存狀態,只要當前組件不被銷燬,那麼狀態就會一直存在。

const status = useRef(false)
/* 改變狀態 */
const handleChangeStatus = () ={
  status.current = true
}

4.3 useImperativeHandle

useImperativeHandle 基礎介紹:

useImperativeHandle 可以配合 forwardRef 自定義暴露給父組件的實例值。這個很有用,我們知道,對於子組件,如果是 class 類組件,我們可以通過 ref 獲取類組件的實例,但是在子組件是函數組件的情況,如果我們不能直接通過 ref 的,那麼此時 useImperativeHandle 和 forwardRef 配合就能達到效果。

useImperativeHandle 接受三個參數:

useImperativeHandle 基礎用法:

我們來模擬給場景,用 useImperativeHandle,使得父組件能讓子組件中的 input 自動賦值並聚焦。

function Son (props,ref) {
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 聲明方法用於聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 聲明方法用於改變input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="請輸入內容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node =(this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子組件</button>
        </div>
    }
}

效果:

8e8c05f0c82c43719079d4db9536abc0_tplv-k3u1fbpfcp-watermark.gif

五 hooks 之狀態派生與保存

5.1 useMemo

useMemo 可以在函數組件 render 上下文中同步執行一個函數邏輯,這個函數的返回值可以作爲一個新的狀態緩存起來。那麼這個 hooks 的作用就顯而易見了:

場景一:在一些場景下,需要在函數組件中進行大量的邏輯計算,那麼我們不期望每一次函數組件渲染都執行這些複雜的計算邏輯,所以就需要在 useMemo 的回調函數中執行這些邏輯,然後把得到的產物(計算結果)緩存起來就可以了。

場景二:React 在整個更新流程中,diff 起到了決定性的作用,比如 Context 中的 provider 通過 diff value 來判斷是否更新

useMemo 基礎介紹:

const cacheSomething = useMemo(create,deps)

useMemo 基礎用法:

派生新狀態:

function Scope() {
    const keeper = useKeep()
    const { cacheDispatch, cacheList, hasAliveStatus } = keeper
   
    /* 通過 useMemo 得到派生出來的新狀態 contextValue  */
    const contextValue = useMemo(() ={
        return {
            cacheDispatch: cacheDispatch.bind(keeper),
            hasAliveStatus: hasAliveStatus.bind(keeper),
            cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
        }
      
    }[keeper])
    return <KeepaliveContext.Provider value={contextValue}>
    </KeepaliveContext.Provider>
}

如上通過 useMemo 得到派生出來的新狀態 contextValue ,只有 keeper 變化的時候,才改變 Provider 的 value 。

緩存計算結果:

function Scope(){
    const style = useMemo(()=>{
      let computedStyle = {}
      // 經過大量的計算
      return computedStyle
    },[])
    return <div style={style} ></div>
}

緩存組件, 減少子組件 rerender 次數:

function Scope ({ children }){
   const renderChild = useMemo(()=>{ children()  },[ children ])
   return <div>{ renderChild } </div>
}

5.2 useCallback

useCallback 基礎介紹:

useMemo 和 useCallback 接收的參數都是一樣,都是在其依賴項發生變化後才執行,都是返回緩存的值,區別在於 useMemo 返回的是函數運行的結果,useCallback 返回的是函數,這個回調函數是經過處理後的也就是說父組件傳遞一個函數給子組件的時候,由於是無狀態組件每一次都會重新生成新的 props 函數,這樣就使得每一次傳遞給子組件的函數都發生了變化,這時候就會觸發子組件的更新,這些更新是沒有必要的,此時我們就可以通過 usecallback 來處理此函數,然後作爲 props 傳遞給子組件。

useCallback 基礎用法:

/* 用react.memo */
const DemoChildren = React.memo((props)=>{
   /* 只有初始化的時候打印了 子組件更新 */
    console.log('子組件更新')
   useEffect(()=>{
       props.getInfo('子組件')
   },[])
   return <div>子組件</div>
})

const DemoUseCallback=({ id })=>{
    const [number, setNumber] = useState(1)
    /* 此時usecallback的第一參數 (sonName)=>{ console.log(sonName) }
     經過處理賦值給 getInfo */
    const getInfo  = useCallback((sonName)=>{
          console.log(sonName)
    },[id])
    return <div>
        {/* 點擊按鈕觸發父組件更新 ,但是子組件沒有更新 */}
        <button onClick={ ()=>setNumber(number+1) } >增加</button>
        <DemoChildren getInfo={getInfo} />
    </div>
}

六 hooks 之工具 hooks

6.1 useDebugValue

我們不推薦你向每個自定義 Hook 添加 debug 值。當它作爲共享庫的一部分時才最有價值。在某些情況下,格式化值的顯示可能是一項開銷很大的操作。除非需要檢查 Hook,否則沒有必要這麼做。因此,useDebugValue 接受一個格式化函數作爲可選的第二個參數。該函數只有在 Hook 被檢查時纔會被調用。它接受 debug 值作爲參數,並且會返回一個格式化的顯示值。

useDebugValue 基礎介紹:

useDebugValue 可用於在 React 開發者工具中顯示自定義 hook 的標籤。這個 hooks 目的就是檢查自定義 hooks。

useDebugValue 基本使用:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在開發者工具中的這個 Hook 旁邊顯示標籤
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

6.2 useId

useID 基礎介紹:

useId 也是 React v18 產生的新的 hooks , 它可以在 client 和 server 生成唯一的 id , 解決了在服務器渲染中,服務端和客戶端產生 id 不一致的問題,更重要的是保障了 React v18 中 streaming renderer (流式渲染) 中 id 的穩定性。

低版本 React ssr 存在的問題:

比如在一些項目或者是開源庫中用 Math.random() 作爲 ID 的時候,可以會有一些隨機生成 id 的場景:

const rid = Math.random() + '_id_'  /* 生成一個隨機id  */
function Demo (){
   // 使用 rid 
   return <div id={rid} ></div>
}

這在純客戶端渲染中沒有問題,但是在服務端渲染的時候,傳統模式下需要走如下流程:

e54da686-6d8e-4431-a378-c05ac49cb6fb.png

在這個過程中,當服務端渲染到 html 和 hydrate 過程分別在服務端和客戶端進行,但是會走兩遍 id 的生成流程,這樣就會造成 id 不一致的情況發生。useId 的出現能有效的解決這個問題。

useId 基本用法:

function Demo (){
   const rid = useId() // 生成穩定的 id 
   return <div id={rid} ></div>
}

v18 ssr

在 React v18 中 對 ssr 增加了流式渲染的特性 New Suspense SSR Architecture in React 18 , 那麼這個特性是什麼呢?我們來看一下:

在傳統 React ssr 中,如果正常情況下, hydrate 過程如下所示:

WechatIMG6936.jpeg

剛開始的時候,因爲服務端渲染,只會渲染 html 結構,此時還沒注入 js 邏輯,所以我們把它用灰色不能交互的模塊表示。(如上灰色的模塊不能做用戶交互,比如點擊事件之類的。)

hydrate js 加載之後,此時的模塊可以正常交互,所以用綠色的模塊展示。

但是如果其中一個模塊,服務端請求數據,數據量比較大,耗費時間長,我們不期望在服務端完全形成 html 之後在渲染,那麼 React 18 給了一個新的可能性。可以使用包裝頁面的一部分,然後讓這一部分的內容先掛起。

接下來會通過 script 加載 js 的方式 流式注入 html 代碼的片段,來補充整個頁面。接下來的流程如下所示:

d94d8ddb-bdcd-4be8-a851-4927c7966b99.png

在這個原理基礎之上, React 這個特性叫 Selective Hydration,可以根據用戶交互改變 hydrate 的順序

比如有兩個模塊都是通過 Suspense 掛起的,當兩個模塊發生交互邏輯時,會根據交互來選擇性地改變 hydrate 的順序。

ede45613-9994-4e77-9f50-5b7c1faf1160.png

如上 C D 選擇性的 hydrate 就是 Selective Hydration 的結果。那麼回到主角 useId 上,如果在 hydrate 過程中,C D 模塊 id 是動態生成的,比如如下:

let id = 0
function makeId(){
  return id++
}
function Demo(){
  const id = useRef( makeId() )
  return <div id={id}  >...</div>
}

那麼如果組件是 Selective Hydration , 那麼註冊組件的順序服務端和客戶端有可能不統一,這樣表現就會不一致了。那麼用 useId 動態生成 id 就不會有這個問題產生了,所以說 useId 保障了 React v18 中 streaming renderer (流式渲染) 中 id 的穩定性。

七 總結

本文詳細介紹了 React Hooks 產生初衷以及 React Hooks,希望看到這篇文章的同學,可以記住每一個 hooks 的使用場景,在項目中熟練使用起來。

參考文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/TWaGu218rdiEDytjEA_ikA