React Hooks 指北

前言

這篇文章旨在總結(jié) React Hooks 的使用技巧以及在使用過程中需要注意的問題,其中會(huì)附加一些問題產(chǎn)生的原因以及解決方式。但是請注意,文章中所給出的解決方式并不一定完全適用,解決問題的方案有很多種,也許你所在的團(tuán)隊(duì)針對這些問題已經(jīng)給出了對應(yīng)的規(guī)范,亦或是你已經(jīng)對這些問題的解決方式形成了更好的認(rèn)知。所以你的著重點(diǎn)應(yīng)該放在 你是否在使用 React hooks 過程中意識(shí)到了這些問題 以及 你對這些問題的思考

useState hook

初始化狀態(tài)

如果組件的某個(gè)狀態(tài)需要依靠大量計(jì)算得到初始值,一般我們會(huì)定義一個(gè)函數(shù)來初始化狀態(tài)

  • 在 class 組件中
state = {
    state1: calcInitialState()
}

沒什么問題,在組件不被重新掛載的情況下,即使組件多次重新渲染,calcInitialState 也只會(huì)被執(zhí)行一次

  • 而在函數(shù)組件中, 有兩種方式
const state1 = useState(calcInitialState) // 組件多次渲染時(shí),calcInitialState 僅會(huì)被執(zhí)行一次
const state1 = useState(calcInitialState()) // 組件每次渲染時(shí),calcInitialState 都會(huì)被執(zhí)行

函數(shù)組件每次重新渲染,都會(huì)執(zhí)行函數(shù)組件本身,在第一次渲染時(shí),useState 會(huì)讀取初始值,如果是初始值函數(shù),則會(huì)被執(zhí)行,并且函數(shù)的返回值被作為初始狀態(tài),此時(shí),這兩種寫法表現(xiàn)相同。但是在后續(xù)重新渲染過程,useState 雖然不會(huì)讀取默認(rèn)狀態(tài)值,也不會(huì)對默認(rèn)狀態(tài)值做任何處理,但是第二種寫法中的 calcInitialState 仍然會(huì)被執(zhí)行,且是毫無意義的。內(nèi)部運(yùn)行流程見源碼 mountStateupdateState

狀態(tài)的捕獲方式 (this & 閉包)

前段時(shí)間,我在團(tuán)隊(duì)內(nèi)部分享了常用的 React Hooks 原理以及源碼,當(dāng)時(shí)我提到了,不論是 class 組件還是函數(shù)組件, 他們的狀態(tài)都存儲(chǔ)在組件對應(yīng)的 fiber 上。函數(shù)組件 和 class 組件狀態(tài)更新的流程如下圖所示:

file

詳細(xì)可見源碼 updateClassInstanceupdateReducer

對于渲染的那一部分(JSX)來說,可以一直拿到最新的的狀態(tài)。只是獲取狀態(tài)的方式不同,class 組件是通過 this.state 指向 fiber 節(jié)點(diǎn)上存儲(chǔ)的狀態(tài),函數(shù)組件則是通過 useState 這個(gè)函數(shù)的返回值獲取,那么問題在于函數(shù)組件拿到的狀態(tài)是存儲(chǔ)在閉包中的,這個(gè)閉包由 useState 執(zhí)行產(chǎn)生。

換個(gè)角度來說,對于函數(shù)組件,我們需要特別重視“渲染”這個(gè)概念。函數(shù)組件每次渲染,其內(nèi)部聲明的函數(shù)或者是返回的 UI(JSX) 都只能捕獲到當(dāng)前這次渲染的 props 和 state,這對于 UI(JSX) 來說完全沒有問題。 但是對于函數(shù)組件內(nèi)部的函數(shù),特別是延遲回調(diào)的函數(shù),需要特別注意等到回調(diào)函數(shù)執(zhí)行時(shí),回調(diào)函數(shù)中捕獲的 state 和 props 是否是你所期望的。

想要避免函數(shù)組件中閉包問題帶來的困擾,需要理解并記住下面兩句話

  • 函數(shù)組件每一次渲染都有它自己的 props 和 state
  • 函數(shù)組件每一次渲染都有它自己的事件處理函數(shù)

狀態(tài)粒度

狀態(tài)粒度過細(xì)

在編寫 class 組件時(shí),幾乎不用考慮狀態(tài)粒度的問題,因?yàn)殚_發(fā)者總是可以一次性聲明所有狀態(tài)或者一次性更新所有狀態(tài),就像這樣

handleClick = () => {
    this.setState({
    currentPage: 2,
    pageSize: 20,
    total: 100
  })
}

大多數(shù)開發(fā)者并不會(huì)蠢到使用三次 setState 去更新這三個(gè)狀態(tài),但是在函數(shù)組件中只能這樣

const handleClick = () => {
  setCurrent(2)
  setPageSize(20)
  setTotal(100)
}

看到這段代碼,嗯,可能會(huì)讓人感覺到有點(diǎn)不舒服,問題就出在更新粒度過細(xì),事實(shí)上一個(gè)分頁組件的 currentPage,pageSize,total 經(jīng)常會(huì)需要同時(shí)被更新,但是多次觸發(fā) setXXX 還是會(huì)讓人感到隱隱的不安,即使多次觸發(fā)更新可能會(huì)被 React 的 batchUpdate 機(jī)制合并為一次,但是當(dāng) setXXX 方法執(zhí)行脫離了 React 的上下文時(shí)會(huì)觸發(fā)多次更新,例如異步結(jié)束時(shí)的回調(diào)中。

此時(shí),我們可以在 useState 中存儲(chǔ)一個(gè)對象,將相關(guān)聯(lián)的狀態(tài)放在一起。也可以使用 useReducer 來管理多個(gè)狀態(tài)。

狀態(tài)粒度過粗

當(dāng)一個(gè) state 有一定的復(fù)雜度的時(shí)候,我并不推薦暴力的將 class 組件聲明 state 的方式硬生生塞到 useState 中,因?yàn)檫@也許會(huì)將 class 中 state 的粒度過粗的缺陷引入進(jìn)來

問題一:難以發(fā)現(xiàn)可以被復(fù)用的狀態(tài)邏輯

當(dāng)一個(gè)組件的狀態(tài)越來越多,組件的可讀性和可維護(hù)性就會(huì)越來越差,不少人應(yīng)該都深有體會(huì), 就像這樣:

ps :截取自真實(shí)的業(yè)務(wù)代碼

class XXX extends React.Component{
  constructor(props: any) {
    super(props);
    this.state = {
      tableListMap: {},
      showPreview: false,
      showRegModal: false,
      dataSource: [],
      columns: [],
      tablePartitionList: [],
      incrementColumns: [],
      loading: false,
      isChecked: {},
      isShowImpala: false,
      tableListSearch: {},
      schemaList: [],
      fetching: false,
      tableListLoading: false,
      bucketList: [],
      showPreviewPath: false,
      previewPath: '',
      currentObject: { object: [''], index: 0, bucket: '' },
      isCompressed: false,
      matchType: null
    };
  }
}

試想一下,在函數(shù)組件中,一個(gè) useState 里面被塞進(jìn)如此多的狀態(tài),且不談能否發(fā)現(xiàn)其中可復(fù)用的狀態(tài)和邏輯,即便你慧眼如炬,發(fā)現(xiàn)了它跟其他組件之間有可以復(fù)用的狀態(tài)和邏輯。大概率,也很難在保證在當(dāng)前組件(歷史代碼)不會(huì)出問題的情況下將可以復(fù)用的狀態(tài)邏輯提取出來。

問題二:將無關(guān)的狀態(tài)放在同一個(gè) useState 中可能讓狀態(tài)更新變得不好控制

舉個(gè)例子,假如頁面上有個(gè)按鈕,當(dāng)點(diǎn)擊這個(gè)按鈕時(shí),需要同時(shí)從不同的接口中拿到兩份數(shù)據(jù)并渲染到頁面上,此時(shí)如果這兩份數(shù)據(jù)被存放在同一個(gè) useState 中

function DataViewer (props) {
    const [dataMap, setDataMap] = useState({ data1: undefined, data2: undefined })
  
  const loadData1 = async () => {
    if (visible) {
      const data1 = await fetchData1()
      setDataMap({ ...dataMap, data1 })
    }
  }

  const loadData2 = async () => {
    if (visible) {
      const data2 = await fetchData2()
        setDataMap({ ...dataMap, data2 })
    }
  }
  
  const handleClick = () => {
    loadData1()
    loadData2()
  }
 // ...
}

問題很明顯,只要兩個(gè)請求都完成,無論成功還是失敗,dataMap 中都不會(huì)有先完成的那個(gè)請求返回的數(shù)據(jù)。

在 class 組件中,基本上是不會(huì)出現(xiàn)這種問題的,因?yàn)榭偸强梢酝ㄟ^ this 拿到當(dāng)前的最新的狀態(tài),不會(huì)出現(xiàn)多次更新中狀態(tài)覆蓋的問題。當(dāng)然在函數(shù)組件中,也可以使用 useRef 暫存接口數(shù)據(jù),然后一起更新狀態(tài),但需要額外寫一些邏輯,這里就不介紹這種黑科技了。

此時(shí)第一個(gè)解決辦法是,將第一個(gè)接口返回的數(shù)據(jù)用變量暫存起來等到第二個(gè)接口完成再去更新 dataMap,就像這樣

const loadDataMap = async () => {
  if (visible) {
     const data1 = await fetchData1()
     const data2 = await fetchData2()
     setDataMap({ data1, data2 })
  }
}

這樣做帶來的問題是,一定要等第一個(gè)請求完成,才能去發(fā)起第二個(gè)請求,對于用戶體驗(yàn)來說并不友好。

好吧,想要兩個(gè)接口并行,還可以使用 Promise.allSettled 并行的處理兩個(gè)請求

const loadDataMap = () => {
  if (visible) {
     Promise.allSettled([ fetchData1(), fetchData2() ])
        .then(results => {
        //...
     })
  }
}

看起來好像沒什么問題了。但是,如果對接口的狀態(tài)、返回值等有額外的處理邏輯時(shí),你就需要將所有的接口的處理邏輯都塞到 .then 的回調(diào)中,并且這種方法一定要兩個(gè)接口都完成才能更新狀態(tài)然后在頁面中展示數(shù)據(jù),也無法單獨(dú)的檢測到其中的某一個(gè)接口是否處于 pending 狀態(tài),這種方式似乎也不是那么友好。

這樣看來比較完美的解決方式只有兩種

  1. 避開閉包, 你只需要在更新狀態(tài)時(shí)傳入一個(gè)函數(shù)就可以了, 就像這樣
setDataMap(dataMap => ({ ...dataMap, data2 }))
  1. 狀態(tài)切分
const [ data1, setData1 ]  = useState()
const [ data2, setData2 ]  = useState()

解決問題的方式往往不止一種,你需要根據(jù)實(shí)際的業(yè)務(wù)情況自行去選擇你認(rèn)為更加合適的方式。

如何設(shè)計(jì)狀態(tài)粒度

官方文檔的 QA 中如是說道

把所有 state 都放在同一個(gè) useState 調(diào)用中,或是每一個(gè)字段都對應(yīng)一個(gè) useState 調(diào)用,這兩方式都能跑通。當(dāng)你在這兩個(gè)極端之間找到平衡,然后把相關(guān) state 組合到幾個(gè)獨(dú)立的 state 變量時(shí),組件就會(huì)更加的可讀。如果 state 的邏輯開始變得復(fù)雜,我們推薦 用 reducer 來管理它,或使用自定義 Hook。

個(gè)人認(rèn)為,聚合相關(guān)的狀態(tài),拆分無關(guān)的狀態(tài),是一種比較好的實(shí)踐方式。比如將分頁器組件的 currentPage、pageSize、total 三個(gè)狀態(tài)放在同一個(gè) useState 中,將不同請求的返回的數(shù)據(jù)拆分到不同的 useState 中。另外還有一些情況是,狀態(tài)的邏輯比較復(fù)雜,這個(gè)時(shí)候也可以使用 useReducer 來管理狀態(tài),這樣就可以將一些復(fù)雜的邏輯抽離到 reducer 中。

狀態(tài)更新的兩種方式

不論是函數(shù)組件還是 class 組件,更新狀態(tài)的方式都有兩種: setState(newState)setState(oldState => newState),它們之間的差異在于,一個(gè)注重結(jié)果,一個(gè)注重目的。 setState(newState) 用于描述新的狀態(tài),而 setState(oldState => newState) 用于描述新的狀態(tài)與舊的狀態(tài)相比應(yīng)當(dāng)做出什么樣的改變。

這樣說可能有點(diǎn)抽象,簡單來說 setState(newState) 是用新的狀態(tài)替換掉舊的狀態(tài), setState(oldState => newState) 是用來通過舊的狀態(tài)計(jì)算出新的狀態(tài)

useEffect hook

為什么需要 useEffect

從理論上講函數(shù)組件就是單純的用來渲染的,也就是所謂的純函數(shù),事實(shí)上沒有 React Hooks 之前也確實(shí)是這樣的。而其他的操作如數(shù)據(jù)獲取,設(shè)置定時(shí)器,修改 DOM 等都被稱作副作用。

為什么不能直接在函數(shù)組件內(nèi)直接執(zhí)行副作用?

  • 有一些副作用操作可能會(huì)影響到渲染,如修改 DOM
  • 有一些副作用操作是需要清除的,如定時(shí)器
  • 如果直接在函數(shù)組件內(nèi)部直接進(jìn)行副作用操作,那么函數(shù)組件每次重新渲染時(shí)都會(huì)執(zhí)行這些操作,沒法控制這些操作何時(shí)執(zhí)行何時(shí)不執(zhí)行

useEffect 怎樣解決這些問題?

  • useEffect 包裹的函數(shù)會(huì)在瀏覽器渲染完成之后執(zhí)行,保證不會(huì)影響到組件的渲染。另外被 useEffect 包裹的函數(shù)執(zhí)行脫離了函數(shù)組件本身的執(zhí)行上下文,所以不會(huì)對函數(shù)組件本身的執(zhí)行造成影響
  • useEffect 包裹的函數(shù)可以 return 一個(gè)函數(shù),用于清除副作用
  • useEffect 可以傳入依賴項(xiàng)數(shù)組,當(dāng)依賴項(xiàng)變化時(shí)才去執(zhí)行副作用操作

useEffect 的這些特性有點(diǎn)像事件回調(diào),只不過事件回調(diào)函數(shù)的觸發(fā)依靠 dom 事件如點(diǎn)擊、輸入等,而 useEffect 包裹的函數(shù)出發(fā)依靠依賴項(xiàng)的變化。很多時(shí)候,將一些副作用操作放到事件回調(diào)函數(shù)中去執(zhí)行是更好的選擇,這樣就可以不用考慮 useEffect 依賴項(xiàng)的問題了。

useEffect 是如何捕獲 props 和 state 的

useEffect 包裹的函數(shù)中,捕獲 props 和 state 的方式跟普通函數(shù)沒兩樣,依賴于函數(shù)組件本身的執(zhí)行上下文。useEffect 內(nèi)部并沒有做什么如數(shù)據(jù)綁定、依賴 fiber 等特別的事情。因此,函數(shù)組件每一次渲染都有它自己的 effects。在函數(shù)組件渲染完成后,產(chǎn)生的 effects 會(huì)被存儲(chǔ)到組件對應(yīng)的 fiber 上,等待特定的時(shí)機(jī)執(zhí)行這些 effects (副作用)。 即便是 effects 中某些異步回調(diào)執(zhí)行時(shí),頁面已經(jīng)重新渲染了很多次了,這些異步的回調(diào)函數(shù)中捕獲的 props 和 state 還是產(chǎn)生這些 effects 的那次渲染中組件的 state 和 props。

useEffect 的依賴項(xiàng)

哪些應(yīng)該被放在 useEffect 的依賴項(xiàng)中

理論上來說。useEffect 的心智模型更接近于 effect 在某些值變化時(shí)去執(zhí)行,但是有的時(shí)候?yàn)榱吮WC effect 中捕獲的 props 和 state 是你所期望的,你不得不將 effect 中用到的所有的組件內(nèi)的變量都放到依賴項(xiàng)中。如果你在項(xiàng)目中設(shè)置了對應(yīng)的 lint 規(guī)則,lint 工具也會(huì)告訴你應(yīng)該這樣做,但是這好像與 useEffect 的心智模型產(chǎn)生了一些沖突。

這種沖突帶來的后果是,effect 可能會(huì)頻繁的執(zhí)行,如下例是一個(gè)每秒遞增的計(jì)數(shù)器

function Counter () {
  const [count, setCount] = useState(1)
  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(timerId);
  }, [count]);
  
  return (
    <>{count}</>
  )
}

如果移除了這個(gè) useEffect 的依賴項(xiàng)中的 count,那么定時(shí)器中的回調(diào)函數(shù)就會(huì)一直執(zhí)行 setCount(1 + 1),這不是我們所期望的。好吧,老實(shí)一點(diǎn),將 count 放到依賴項(xiàng)中,但是此時(shí)定時(shí)器會(huì)被頻繁的清除和創(chuàng)建,這可能會(huì)影響定時(shí)器回調(diào)的觸發(fā)頻率,這也不是我們所期望的。

到現(xiàn)在為止,問題還是沒有得到解決,我還是傾向于 useEffect 的依賴項(xiàng)是用來觸發(fā) effect 的,而不是用來解決閉包問題的,那么只能想辦法移除掉 useEffect 對 count 的依賴。

如何減少 useEffect 的依賴項(xiàng)

  • 消除 useEffect 中不必要的捕獲
    如上例中的 useEffect 可以寫成
useEffect(() => {
  const timerId = setInterval(() => {
    setCount(count => count + 1);
  }, 1000);
  return () => clearInterval(timerId);
}, []);
  • 將依賴從 effect 中解耦
    還是這個(gè)定時(shí)計(jì)數(shù)器的例子,如果我們想要通過 props 傳遞一個(gè) step 屬性給這個(gè)組件,用來告訴這個(gè)組件每秒遞增的值的大小
<Counter step={2}/>

于是 Counter 組件就變成了這樣

function Counter ({step}) {
  const [count, setCount] = useState(1)
  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count => count + step);
    }, 1000);
    return () => clearInterval(timerId);
  }, [step]);
  
  return (
    <>{count}</>
  )
}

現(xiàn)在的問題是,當(dāng) step 的值變化時(shí),仍然會(huì)重啟定時(shí)器。現(xiàn)在該 useReducer 上場了。

function Counter ({step}) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step;
    } else {
      throw new Error('type in action is not true');
    }
  }

  useEffect(() => {
    const timerId = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(timerId);
  }, []);
  
  
  return (
    <>{count}</>
  )
}

那么現(xiàn)在問題來了:
Q:為什么我們可以不在依賴項(xiàng)中加入 _dispatch_ ?
A:因?yàn)?React 向我們保證了 _dispatch_ (來源于useReducer)setState (來源于useState)以及ref.current (來源于useRef)即使組件多次渲染,它們的引用地址也不會(huì)改變
Q:為什么 reducer 中捕獲到的 step 值是最新的?
A:對于 useReducer 來說,React 只會(huì)記住它的 action,并不會(huì)記住它的 reducer。也就是說每次組件重新渲染執(zhí)行 useReducer 時(shí),它都會(huì)重新讀取 reducer
上文中說過 useStateuseReducer 在源碼中是同一個(gè)東西。在實(shí)際使用過程中,相比于 useState ,useReducer 可以讓我們把 更新邏輯(reducer)描述發(fā)生了什么(action)分開 ,而這一點(diǎn)正好可以用來移除不必要的依賴。

當(dāng)然實(shí)際業(yè)務(wù)中,很少會(huì)碰到上述例子中的場景,絕大部分情況下,都只用將想要用來觸發(fā)執(zhí)行 effect 的值放到 useEffect 的依賴項(xiàng)中。對于無法用上面講的兩種方法解決的場景,也可以通過 useRef 來繞過煩人的閉包問題。

函數(shù)應(yīng)當(dāng)作為 useEffect 的依賴項(xiàng)嗎

個(gè)人觀點(diǎn)是:絕大多數(shù)情況下,不應(yīng)該將函數(shù)作為 useEffect 的依賴項(xiàng)。至于能否安心的將函數(shù)從依賴項(xiàng)中移除主要看

  • 函數(shù)是否參與 React 的數(shù)據(jù)流
    簡單來說,就是看這個(gè)函數(shù)中是否用到了函數(shù)組件內(nèi)部的變量(useRef 除外)。如果一個(gè)函數(shù)并沒有參與 React 數(shù)據(jù)流,但是在 useEffect 中用到了,此時(shí)你應(yīng)該將這個(gè)函數(shù)提取到組件外部,這樣你就可以在 useEffect 的依賴項(xiàng)中無腦移除掉這個(gè)函數(shù)。
  • 函數(shù)是否被異步延遲調(diào)用
    函數(shù)被延遲調(diào)用的情況下很容易產(chǎn)生閉包問題,這時(shí)即使將函數(shù)作為 useEffect 的依賴項(xiàng),也無法解決閉包問題,反而可能增加 effect 的觸發(fā)頻率,下文中的 使用可變數(shù)據(jù)替代不可變數(shù)據(jù) 一節(jié)會(huì)介紹一種方法能夠保證在函數(shù)引用地址不變的情況下,使函數(shù)自動(dòng)捕組件內(nèi)變量最新的值的方法。

大多數(shù)情況下都可以不用將函數(shù)放在 useEffect 的依賴項(xiàng)中。也許有一些極端特殊的業(yè)務(wù)場景,這時(shí)只能將函數(shù)用 useCallback 包裹,然后放到 useEffect 的 依賴項(xiàng)中 (我目前沒碰到過這種情況)

useRef hook

按照我個(gè)人的理解,useRef 更像是 class 組件的實(shí)例屬性,即 this.xxx。在函數(shù)組件中,useRef 可以看做是一個(gè)容器,你可以任意操作這個(gè)容器中的數(shù)據(jù),并且這個(gè)容器中的引用地址不會(huì)因?yàn)榻M件多次重新渲染而改變。在我看來它就是函數(shù)組件的作弊器同時(shí)也是解決函數(shù)組件中閉包問題的絕世利器。

useRef 的特征

  1. 當(dāng) useRef 存儲(chǔ)的值變化時(shí),并不會(huì)引起組件重新渲染
  2. 可以用來存放可變數(shù)據(jù),在組件多次渲染時(shí),能保持 ref (useRef 返回值)本身的引用地址不變

_ps: useRef 能保證返回值引用地址不變的原因是,即使組件多次渲染,useRef 返回的 ref 還是第一次執(zhí)行時(shí)返回的那個(gè) ref。在函數(shù)組件第一次渲染時(shí), React 內(nèi)部會(huì)將 useRef 的返回值(ref)存儲(chǔ)在組件對應(yīng)的 fiber 節(jié)點(diǎn)上,后續(xù)組件重新渲染時(shí),React 內(nèi)部不會(huì)對 useRef 做任何處理,直接返回 fiber 節(jié)點(diǎn)上存儲(chǔ)的 ref。詳情可見源碼 mountRef 和 _updateRef

基于 useRef 的特征可以做什么

  • 實(shí)現(xiàn)一個(gè)自定義 hook 用來統(tǒng)計(jì)組件的渲染次數(shù)
const useRenderTimes = () => {
  const ref = useRef(0)
  ref.current += 1
  return ref.current
}
  • 記錄上一次組件渲染時(shí)的某個(gè)值
const usePreValue = (value) => {
  const ref = useRef(undefined)
  const preValue = ref.current
  ref.current = value
  return preValue
}
  • 避開不必要的重新渲染
    如果某一個(gè)狀態(tài)與渲染無關(guān),那么你可以使用 useRef 代替 useState 。還記得上述 useEffect 那一節(jié)中的 Counter 組件嗎,如果將 count 作為 useEffect 的依賴項(xiàng),那么定時(shí)器會(huì)不停的創(chuàng)建/銷毀,上面給出了兩種解決方法,現(xiàn)在我們來說說另一種方式。思路是,只要在 count 變化時(shí),不重新渲染組件就好了, 那么可以使用 useRef 存儲(chǔ) count 值,當(dāng)然,這種方式的僅限于 count 不參與渲染的情況,或者也可以在 useRef 中存儲(chǔ)值改變的同時(shí)去觸發(fā)組件重新渲染。
    這樣的場景其實(shí)很常見,比如有一個(gè)表單,當(dāng)用戶在表單中填寫完成后,點(diǎn)擊 submit 按鈕,將數(shù)據(jù)通過接口發(fā)送給后端。如果這個(gè)表單不是一個(gè)受控組件,那么相比于 useState 用 useRef 存儲(chǔ)表單數(shù)據(jù)是個(gè)更好的選擇,因?yàn)?strong>它不會(huì)導(dǎo)致不必要的重新渲染。
function Counter () {
  const count = useRef(1)
  useEffect(() => {
    const timerId = setInterval(() => {
      count.current += 1;
    }, 1000);
    return () => {
      clearInterval(timerId)
      console.log(count.current)
    };
  }, []);
}

有時(shí)在函數(shù)組件中使用 useState 是為了在組件重新渲染之后仍然能拿到某個(gè)值,但我們希望讓這個(gè)值變化時(shí)不要觸發(fā)組件更新,亦或是想避免 useState 的不可變數(shù)據(jù)導(dǎo)致的閉包問題,那么這個(gè)時(shí)候就是使用 useRef 的時(shí)機(jī)。

如何看待 useRef

在我看來在 React Hooks 中 useRef 最起碼與 useState是同等重要的,知乎有篇文章中的一句話這樣說,

每一個(gè)希望深入 hook 實(shí)踐的開發(fā)者都必須記住這個(gè)結(jié)論,無法自如地使用 useRef 會(huì)讓你失去 hook 將近一半的能力。

表示認(rèn)同。

useCallback Hook

關(guān)于 useCallback,官網(wǎng)上的介紹是

把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項(xiàng)數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個(gè)依賴項(xiàng)改變時(shí)才會(huì)更新。當(dāng)你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時(shí),它將非常有用。

又看到了熟悉的詞匯-“依賴項(xiàng)”,要想保證在 useCallback 中包裹的函數(shù)捕獲到當(dāng)前渲染時(shí)函數(shù)組件內(nèi)部的值,必須將 useCallback 包裹的函數(shù)中所有引用到的函數(shù)組件內(nèi)部的值都放到依賴項(xiàng)中。另外,請注意官網(wǎng)介紹的 useCallback 的作用是- “性能優(yōu)化”。

你真的需要為函數(shù)組件中的每一個(gè)函數(shù)都包裹上 useCallback 嗎? 就拿官方文檔中的 shouldComponentUpdate 舉例,我們在函數(shù)組件中定義了一個(gè)函數(shù) handleClick 并用 useCallback 包裹,然后通過 props 傳遞給子組件,子組件中通過shouldComponentUpdate 對比 handleClick ,決定是否需要更新。

function Parent () {
  const handleClick = useCallback(()=>{
    //...
  },[...])
  
  return (<Child handleClick={handleClick}/>)
}

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    return this.props.handleClick !== nextProps.handleClick
  }
  // ...
}

那么此時(shí)應(yīng)該有一個(gè)疑問,性能提升到底有多大,如果你感興趣的話,不妨動(dòng)起手來,寫個(gè)示例,對比一下 performance 性能面板,你應(yīng)該看到 useCallback 對性能提升到底有多大,同時(shí)根據(jù)測試結(jié)果,可以大概得到什么時(shí)候應(yīng)該用 useCallback 來提升性能。useCallback 的另一個(gè)作用是可以維持函數(shù)引用地址不變。但是它仍然會(huì)在依賴項(xiàng)變化時(shí)重新生成函數(shù),想要維持函數(shù)引用地址一直不變還要是要使用 useRef

我抵觸 useCallback 的原因是,在我看來它本身的作用比較雞肋,而且使用 useCallback,必須注意依賴項(xiàng),這又還會(huì)帶來額外的心智負(fù)擔(dān)。

useMemo Hook

使用 useMemo 時(shí)需要注意的點(diǎn)不多,官方文檔也寫的非常明白了

useMemo 的作用是

把“創(chuàng)建”函數(shù)和依賴項(xiàng)數(shù)組作為參數(shù)傳入 useMemo,它僅會(huì)在某個(gè)依賴項(xiàng)改變時(shí)才重新計(jì)算 memoized 值。這種優(yōu)化有助于避免在每次渲染時(shí)都進(jìn)行高開銷的計(jì)算。

請把著重點(diǎn)放在 “高開銷的計(jì)算” 上,有的時(shí)候,可能也并不需要 useMemo

使用 useMemo 時(shí)需要注意的是

你可以把 **useMemo** 作為性能優(yōu)化的手段,但不要把它當(dāng)成語義上的保證。將來,React 可能會(huì)選擇“遺忘”以前的一些 memoized 值,并在下次渲染時(shí)重新計(jì)算它們,比如為離屏組件釋放內(nèi)存。先編寫在沒有 useMemo 的情況下也可以執(zhí)行的代碼 —— 之后再在你的代碼中添加 useMemo,以達(dá)到優(yōu)化性能的目的。

函數(shù)組件中的閉包問題

結(jié)合上文,可以總結(jié)出在函數(shù)組件中,閉包問題主要是因?yàn)楹瘮?shù)的延遲調(diào)用,不論是 useEffect 包裹的函數(shù)還是定時(shí)器回調(diào)函數(shù)亦或者是異步請求的回調(diào)函數(shù),它們內(nèi)部捕獲到的變量都是存在外部函數(shù)組件執(zhí)行時(shí)產(chǎn)生的閉包中,那么想要規(guī)避閉包帶來的困擾,思路有兩個(gè)

減少函數(shù)內(nèi)部對外部變量的依賴

比如上述定時(shí)計(jì)數(shù)器例子中

setCount(count + 1)
// 替換為
setCount(count => count + 1)

使用可變數(shù)據(jù)替代不可變數(shù)據(jù)

在 class 組件中很少遇到閉包的困擾是因?yàn)樵?class 組件中訪問組件的 state 和 props 都是通過 this,雖然 this.state 和 this.props 指向的是是不可變數(shù)據(jù),但是 this 內(nèi)部存儲(chǔ)的數(shù)據(jù)是可變的并且 this 的引用地址不會(huì)發(fā)生改變。那么函數(shù)組件中有沒有類似 this 的東西呢?有, useRef。

  • 對于非函數(shù)類型,可以使用 useRef 替代 useState
    這樣即使是延遲調(diào)用的函數(shù),也可以通過 ref.current 取到最新的值,因?yàn)檠舆t調(diào)用的函數(shù)里面取的是 useRef 返回值的引用地址。上文中的例子中也這樣用過了。需要注意的是,如果 useRef 中存儲(chǔ)的值參與了渲染,比如
function demo () {
    const text = useRef("")
  return <>{text.current}</>
}

這時(shí),更新 useRef 中存儲(chǔ)的值,并不會(huì)引起視圖重新渲染。但是我們可以通過更新另一個(gè)狀態(tài) (useState) 來使視圖同步。如果在組件中實(shí)在找不到一個(gè)可以在 useRef 內(nèi)部的值變化時(shí)去觸發(fā)更新的狀態(tài),那么也可以寫一個(gè)自定義 hook 去強(qiáng)制觸發(fā)更新

function useForceUpdate () {
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  return forceUpdate
}

封裝一下,就可以得到一個(gè)存儲(chǔ)可變數(shù)據(jù)的 useState

function useMutableState (init) {
    const stateRef = useRef(init)
    const [, updateState] = useReducer((preState, action) => {
        stateRef.current = typeof action === 'function'
            ? action(preState)
            : action
        return stateRef.current
    }, init)

    return [stateRef, updateState]
}
  • 對于函數(shù)類型,也可以通過 useRef 保持函數(shù)引用地址不變,函數(shù)內(nèi)部自動(dòng)捕獲最新的值
function useStableFn(fn, deps) {
  const fnRef = useRef();
  fnRef.current = fn;
  return useCallback(() => {
    return fnRef.current();
  }, []);
}

結(jié)語

在 React Hooks 中很難總結(jié)出真正完美的最佳實(shí)踐,就連官方文檔和博客上也只是描述了 React Hooks 的心智模型。上文中的有些觀點(diǎn)或者示例違背了官方給出的心智模型,不得不承認(rèn)我是 useRef 的愛好者。但是對于 React Hooks 的實(shí)踐來說,沒有銀彈。重要的是,你是否理解 hooks 是如何工作的,以及你有沒有自己的避坑指南。

參考鏈接

這篇文章旨在總結(jié) React Hooks 的使用技巧以及在使用過程中需要注意的問題,其中會(huì)附加一些問題產(chǎn)生的原因以及解決方式。但是請注意,文章中所給出的解決方式并不一定完全適用,解決問題的方案有很多種,也許你所在的團(tuán)隊(duì)針對這些問題已經(jīng)給出了對應(yīng)的規(guī)范,亦或是你已經(jīng)對這些問題的解決方式形成了更好的認(rèn)知。所以你的著重點(diǎn)應(yīng)該放在你是否在使用 React hooks 過程中意識(shí)到了這些問題以及你對這些問題的思考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、react新特性 1. context 在一個(gè)典型的 React 應(yīng)用中,數(shù)據(jù)是通過 props 屬性自上而下...
    zxhnext閱讀 1,116評論 0 0
  • Hooks 在 16.8 版本中被添加到 React,允許函數(shù)組件訪問狀態(tài)和其他 React 特性。因此,通常不再...
    lio_zero閱讀 451評論 0 2
  • React Hooks Hook是React v16.8的新特性,可以用函數(shù)的形式代替原來的繼承類的形式,可以在不...
    左冬的博客閱讀 888評論 0 1
  • 大綱 ?? 函數(shù)式編程?? 什么是純函數(shù)?? 什么是副作用(Effect)?? 為什么要使用純函數(shù) ?? React函數(shù)組件...
    這個(gè)前端不太冷閱讀 1,242評論 0 1
  • 1.useState 使用單個(gè) state 變量還是多個(gè) state 變量useState 的出現(xiàn),讓我們可以使用...
    MelousJ閱讀 771評論 0 0

友情鏈接更多精彩內(nèi)容