【實戰總結篇】寫React Hooks前必讀

2021-03-02 前端人

最近團隊內有同學,由於寫react hooks引發了一些bug,甚至有1例是線上問題。團隊內也因此發起了一些爭執,到底要不要寫hooks?到底要不要加lint?到底要不要加autofix?爭論下來結論如下:

團隊再出一篇必讀文檔,必須要求每位同學,先讀再寫。

因此便有了此文。

本文主要講兩大點:

硬性要求1. 必須完整閱讀一次React Hooks官方文檔

英文文檔:https://reactjs.org/docs/hooks-intro.html
中文文檔:https://zh-hans.reactjs.org/docs/hooks-intro.html
其中重點必看hooks: useState、useReducer、useEffect、useCallback、useMemo

另外推薦閱讀:

https://link.zhihu.com/?target=https%3A//overreacted.io/zh-hans/a-complete-guide-to-useeffect/

https://zhuanlan.zhihu.com/p/92211533

2. 工程必須引入lint插件,並開啟相應規則

lint插件:https://www.npmjs.com/package/eslint-plugin-react-hooks
必開規則:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

其中, react-hooks/exhaustive-deps 至少warn,也可以是error。建議全新的工程直接配"error",歷史工程配"warn"。

切記,本條是硬性條件。

如果你的工程,當前沒開啟hooks lint rule,請不要編寫任何hooks代碼。如果你CR代碼時,發現對方前端工程,沒有開啟相應規則,並且提交了hooks代碼,請不要合併。該要求適應於任何一個React前端工程。

這兩條規則會避免我們踩坑。雖然對於hooks新手,這個過程可能會比較「痛苦」。不過,如果你覺得這兩個規則對你編寫代碼造成了困擾,那說明你還未完全掌握hooks。

如果對於某些場景,確實不需要「exhaustive-deps」,可在代碼處加:
// eslint-disable-next-line react-hooks/exhaustive-deps

切記只能禁本處代碼,不能偷懶把整個文件都禁了。

3. 如若有發現hooks相關lint導致的warning,不要全局autofix

除了hooks外,正常的lint基本不會改變代碼邏輯,只是調整編寫規範。但是hooks的lint規則不同,exhaustive-deps 的變化會導致代碼邏輯發生變化,這極容易引發線上問題,所以對於hooks的waning,請不要做全局autofix操作。除非保證每處邏輯都做到了充分回歸。

另外公司內部有個小姐姐補充道:eslint-plugin-react-hooks 從2.4.0版本開始,已經取消了 exhaustive-deps 的autofix。所以,請儘量升級工程的lint插件至最新版,減少出錯風險

然後建議開啟vscode的「autofix on save」。未來無論是什麼問題,能把error與warning 儘量遏制在最開始的開發階段,保證自測跟測試時就是符合規則的代碼。

常見注意點依賴問題

依賴與閉包問題是一定要開啟exhaustive-deps 的核心原因。最常見的錯誤即:mount時綁定事件,後續狀態更新出錯。

錯誤代碼示例:(此處用addEventListener做onclick綁定,只是為了方便說明情況)

function ErrorDemo() {
  const [count, setCount] = useState(0);
  const dom = useRef(null);
  useEffect(() => {
    dom.current.addEventListener('click', () => setCount(count + 1));
  }, []);
  return <div ref={dom}>{count}</div>;
}

這段代碼的初始想法是:每當用戶點擊dom,count就加1。理想中的效果是一直點,一直加。但實際效果是 {count} 到「1」以後就加不上了。

我們來梳理一下, useEffect(fn, []) 代表只會在mount時觸發。也即是首次render時,fn執行一次,綁定了點擊事件,點擊觸發 setCount(count + 1) 。乍一想,count還是那個count,肯定會一直加上去呀,當然現實在啪啪打臉。

狀態變更 觸發 頁面渲染的本質是什麼?本質就是 ui = fn(props, state, context) 。props、內部狀態、上下文的變更,都會導致渲染函數(此處就是ErrorDemo)的重新執行,然後返回新的view。

那現在問題來了, ErrorDemo 這個函數執行了多次,第一次函數內部的 count 跟後面幾次的 count 會有關係嗎?這麼一想,感覺又應該沒有關係了。那為什麼 第二次又知道 count 是1,而不是0了呢?第一次的setCount 跟後面的是同一個函數嗎?這背後涉及到hooks的一些底層原理,也關係到了為什麼hooks的聲明需要聲明在函數頂部,不允許在條件語句中聲明。在這裡就不多講了。

結論是:每次 count 都是重新聲明的變量,指向一個全新的數據;每次的setCount 雖然是重新聲明的,但指向的是同一個引用。

回到正題,我們知道了每次render,內部的count其實都是全新的一個變量。那我們綁定的點擊事件方法,也即:setCount(count + 1) ,這裡的count,其實指的一直是首次render時的那個count,所以一直是0 ,因此 setCount,一直是設置count為1。

那這個問題怎麼解?

首先,應該遵守前面的硬性要求,必須要加lint規則,並開啟autofix on save。然後就會發現,其實這個 effect 是依賴 count 的。autofix 會幫你自動補上依賴,代碼變成這樣:

useEffect(() => {
  dom.current.addEventListener('click', () => setCount(count + 1));
}, [count]);

那這樣肯定就不對了,相當於每次count變化,都會重新綁定一次事件。所以對於事件的綁定,或者類似的場景,有幾種思路,我按我的常規處理優先級排列:

思路1:消除依賴
在這個場景裡,很簡單,我們主要利用 setCount 的另一個用法 functional updates。這樣寫就好了:
() => setCount(prevCount => ++prevCount) ,不用關心什麼新的舊的、什麼閉包,省心省事。

思路2:重新綁定事件
那如果我們這個事件就是要消費這個count怎麼辦?比如這樣:

dom.current.addEventListener('click', () => {
  console.log(count);
  setCount(prevCount => ++prevCount);
});

我們不必執著於一定只在mount時執行一次。也可以每次重新render前移除事件,render後綁定事件即可。這裡利用useEffect的特性,具體可以自己看文檔:

useEffect(() => {
  const $dom = dom.current;
  const event = () => {
    console.log(count);
    setCount(prev => ++prev);
  };
  $dom.addEventListener('click', event);
  return () => $dom.removeEventListener('click', event);
}, [count]);

思路3:如果嫌這樣開銷大,或者編寫麻煩,也可以用 useRef
其實用 useRef 也挺麻煩的,我個人不太喜歡這樣操作,但也能解決問題,代碼如下:

const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
  dom.current.addEventListener('click', () => {
    console.log(countRef.current);
    setCount(prevCount => {
      const newCount = ++prevCount;
      countRef.current = newCount;
      return newCount;
    });
  });
}, []);

useCallback與useMemo

這兩個api,其實概念上還是很好理解的,一個是「緩存函數」, 一個是緩存「函數的返回值」。但我們經常會懶得用,甚至有的時候會用錯。

從上面依賴問題我們其實可以知道,hooks對「有沒有變化」這個點其實很敏感。如果一個effect內部使用了某數據或者方法。若我們依賴項不加上它,那很容易由於閉包問題,導致數據或方法,都不是我們理想中的那個它。如果我們加上它,很可能又會由於他們的變動,導致effect瘋狂的執行。真實開發的話,大家應該會經常遇到這種問題。

所以,在此建議:

在組件內部,那些會成為其他useEffect依賴項的方法,建議用 useCallback 包裹,或者直接編寫在引用它的useEffect中。己所不欲勿施於人,如果你的function會作為props傳遞給子組件,請一定要使用 useCallback 包裹,對於子組件來說,如果每次render都會導致你傳遞的函數發生變化,可能會對它造成非常大的困擾。同時也不利於react做渲染優化。

不過還有一種場景,大家很容易忽視,而且還很容易將useCallback與useMemo混淆,典型場景就是:節流防抖。

舉個例子:

function BadDemo() {
  const [count, setCount] = useState(1);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  return <div onClick={handleClick}>{count}</div>;
}

我們希望防止用戶連續點擊觸發多次變更,加了防抖,停止點擊1秒後才觸發 count + 1 ,這個組件在理想邏輯下是OK的。但現實是骨感的,我們的頁面組件非常多,這個 BadDemo 可能由於父級什麼操作就重新render了。現在假使我們頁面每500毫秒會重新render一次,那麼就是這樣:

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  useEffect(() => {
    // 每500ms,組件重新render
    window.setInterval(() => {
      setRerender(r => !r);
    }, 500);
  }, []);
  return <div onClick={handleClick}>{count}</div>;
}

每次render導致handleClick其實是不同的函數,那麼這個防抖自然而然就失效了。這樣的情況對於一些防重點要求特別高的場景,是有著較大的線上風險的。

那怎麼辦呢?自然是想加上 useCallback :

const handleClick = useCallback(debounce(() => {
  setCount(c => ++c);
}, 1000), []);

現在我們發現效果滿足我們期望了,但這背後還藏著一個驚天大坑。
假如說,這個防抖的函數有一些依賴呢?比如 setCount(c => ++c); 變成了 setCount(count + 1) 。那這個函數就依賴了 count 。代碼就變成了這樣:

const handleClick = useCallback(
  debounce(() => {
    setCount(count + 1);
  }, 1000),
  []
);

大家會發現,你的lint規則,竟然不會要求你把 count 作為依賴項,填充到deps數組中去。這進而導致了最初的那個問題,只有第一次點擊會count++。這是為什麼呢?

因為傳入useCallback的是一段執行語句,而不是一個函數聲明。只是說它執行以後返回的新函數,我們將其作為了 useCallback 函數的入參,而這個新函數具體是個啥,其實lint規則也不知道。

更合理的姿勢應該是使用 useMemo :

const handleClick = useMemo(
  () => debounce(() => {
    setCount(count + 1);
  }, 1000),
  [count]
);

這樣保證每當 count 發生變化時,會返回一個新的加了防抖功能的新函數。

總而言之,對於使用高階函數的場景,建議一律使用 useMemo

有些網友提供了寶貴的反饋,我繼續補充:剛使用useMemo,依舊存在一些問題。

問題1useMemo「將來」並不「穩定」

react的官方文檔中提到:

你可以把 useMemo 作為性能優化的手段,但不要把它當成語義上的保證。 將來,React 可能會選擇「遺忘」以前的一些 memoized 值,並在下次渲染時重新計算它們,比如為離屏組件釋放內存。先編寫在沒有 useMemo 的情況下也可以執行的代碼 —— 之後再在你的代碼中添加 useMemo,以達到優化性能的目的。

也就是說,在將來的某種特殊情況下,這個防抖函數依舊會失效。當然,這種情況是發生在「將來」,且相對比較極端,出現概率較小,即使出現,也不會「短時間內連續」出現。所以對於不是 「前端防不住抖就要完蛋」的場景,風險相對較小。

問題2useMemo並不能一勞永逸解決所有高階函數場景

在示例的場景中,防抖的邏輯是:「連續點擊後1秒,真正執行邏輯,在這過程中的重複點擊失效」。而如果業務邏輯改成了「點擊後立即發生狀態變更,再之後的1秒內重複點擊無效」,那麼我們的代碼可能就變成了。

const handleClick = useMemo( () => throttle(() => { setCount(count + 1); }, 1000), [count] );

然後發現又失效了。原因是點擊以後,count立即發生了變化,然後handleClick又重複生成了新函數,這個節流就失效了。

所以這種場景,思路又變回了前面提到的,「消除依賴」 或 「使用ref」。當然啦,也可以選擇自己手動實現一個 debounce 或 throttle。我建議可以直接使用社區的庫,比如react-use,或者參考他們的實現自己寫兩個實現。

其他的注意點,後面想到了再持續補充,或者歡迎回復~

來源:相學長 

https://zhuanlan.zhihu.com/p/113216415


console.log("點讚===再看===快樂")

相關焦點

  • 寫React Hooks前必讀
    來源:相學長 https://zhuanlan.zhihu.com/p/113216415最近團隊內有同學,由於寫react hooks引發了一些bug,甚至有1例是線上問題。團隊內也因此發起了一些爭執,到底要不要寫hooks?到底要不要加lint?到底要不要加autofix?
  • 寫React Hooks前需要注意什麼?
    團隊內也因此發起了一些爭執,到底要不要寫hooks?到底要不要加lint?到底要不要加autofix?爭論下來結論如下:團隊再出一篇必讀文檔,必須要求每位同學,先讀再寫。因此便有了此文。本文主要講兩大點:硬性要求1.
  • React Hooks使用小結
    社區,論壇裡各位大佬大多認為,更加的可抽象,邏輯可復用,代碼精簡,避免寫各種生命周期,這裡我就react在官方文檔提到的hooks出現的動機(emmm...不就是好處麼)與大家的使用感受做一下簡單的總結:公共邏輯的復用。
  • 函數式編程看React Hooks(一)簡單React Hooks實現
    以下 三點是 react 官網所提到的 hooks 的動機 https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation代碼重用:在hooks出來之前,常見的代碼重用方式是 HOC 和render props,這兩種方式帶來的問題是:你需要解構自己的組件,同時會帶來很深的組件嵌套複雜的組件邏輯:在class組件中
  • 30 分鐘精通 React 新特性——React Hooks
    難道是Mixins要在react中死灰復燃了嗎?當然不會了,等會我們再來談兩者的區別。總而言之,這些hooks的目標就是讓你不再寫class,讓function一統江湖。React為什麼要搞一個Hooks?想要復用一個有狀態的組件太麻煩了!我們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可復用的組件,並且用自上而下的單向數據流的形式將這些組件串聯起來。
  • 【React】853- 手摸手教你基於Hooks 的 Redux 實戰姿勢
    Redux 使您可以集中存放 JavaScript 應用程式的狀態(數據)它最常與 React 一起使用(通過 react-redux )這使您可以從樹中的任何組件訪問或更改狀態。要分派 action ,請使用 react-redux 中的自定義 hook: useDispatch用一個 action 對象來調用 useDispatch,將傳入 reducers 函數並運行,有可能改變應用的狀態
  • React Hooks 原理與最佳實踐
    然後我寫了一篇文章,利用 Object.defineProperty 簡單實現了 Composition API,可以參考:用 React Hooks 簡單實現 Vue3 Composition API當然這個實現還有很多問題,也比較簡單,可以參考工業聚寫的完整實現:react-use-setup
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    難道是Mixins要在react中死灰復燃了嗎?當然不會了,等會我們再來談兩者的區別。總而言之,這些hooks的目標就是讓你不再寫class,讓function一統江湖。React為什麼要搞一個Hooks?想要復用一個有狀態的組件太麻煩了!我們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可復用的組件,並且用自上而下的單向數據流的形式將這些組件串聯起來。
  • React 16.8發布:hooks終於來了!
    如果你之前從未聽說過 hooks,可以參考以下這些資源:「Introducing hookss」解釋了我們為 React 添加 hooks 功能:https://reactjs.org/docs/hooks-intro.html「hookss at a Glance」對內置的 hooks 進行了快速的介紹:https://reactjs.org
  • 你可能不知道的 React Hooks
    參考資料[1] Hooks API Reference: https://reactjs.org/docs/hooks-reference.html[2] useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect[3] Hooks API Reference: https://reactjs.org
  • 超性感的React Hooks(四):useEffect
    如果讀懂了,順手給我點個讚,然後那麼這篇文章到這裡就可以完結了。如果沒有讀懂,也沒有關係,一起來學習一下。首先,我們要拋開生命周期的固有思維。許多朋友試圖利用class語法中的生命周期來類比理解useEffect,也許他們認為,hooks只是語法糖而已。
  • ahooks 正式發布:值得擁抱的 React Hooks 工具庫
    方案,這便也是 ahooks 的由來。那麼好奇的你肯定會問, ice/hooks 與標題的 ahooks 的關係是什麼?待我細細道來 😆在 ice/hooks RFC 期間,我們也對比參考了社區的同類方案諸如 react-use 等,但最終因為 react-use 提供的 Hooks 過於冗餘
  • React hooks 最佳實踐【更新中】
    function component 來代替類的寫法;但是俗話說的好,沒有什麼東西是十全十美的,在本次整理總結 hooks 庫的過程中,有體驗到 hooks 帶來的體驗提升,同時也存在對比類生命周期寫法中不足的地方。
  • React v16.8 發布:帶來穩定版的 Hooks 功能
    hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。>從 16.8.0 開始,React 包含穩定的 React Hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer要注意的是,如需使用 hooks
  • 你不知道的 React Hooks(萬字長文,快速入門必備)
    類的 this 指向性問題: 我們用 class 來創建 react 組件時,為了保證 this 的指向正確,我們要經常寫這樣的代碼:const that = this,或者是this.handleClick = this.handleClick.bind(this)>;一旦 this 使用錯誤,各種 bug 就隨之而來。
  • React Hooks 還不如類?
    在這篇文章中,作者按照官方文檔的描述,分析用 React Hooks 代替類的動機,一如標題所示,作者並不是很喜歡這一特性。
  • React Hooks 設計思想
    render 結束後 index 會重置為 0,下一次 render 執行 useState 時會按照相同順序訪問 hooks[index];正是因為 hooks 是這樣實現的,我們在調用 hooks 的時候必須要嚴格保證每一次 render 都能獲得一致的執行順序,所以必須要做到:到目前為止,我們已經可以通過 hooks 的形式管理 state,並通過調用包含 setState
  • React Hook起飛指南
    所以這篇文章不會講很多API,也不會講API的基本用法,只把這兩個能做的事情講清楚,閱讀全文大概5-10分鐘。狀態管理:useState副作用管理:useEffect這兩個api就是hook世界裡的鐮刀和錘子,看似簡單的兩個api實際上所代表的,是相比以前截然不同的一種新的編程模型。
  • 「混合雙打」之如何在 Class Components 中使用 React Hooks
    前情提要React 在 v16.8.0 版本中推出了 Hook,作為純函數組件的增強,給函數組件帶來了狀態、上下文等等;之前一篇關於 React Hooks 的文章介紹了如何使用一些官方鉤子和如何自建鉤子,如果想要了解這些內容的同學可以訪問《看完這篇,你也能把 React Hooks 玩出花》。
  • 使用React Hooks代替類的6大理由
    React hooks 已經出來有一段時間了,但是許多 React 開發人員對它們的態度並不是很積極。我發現這背後主要有兩個原因。第一個原因是許多 React 開發人員已經參與了一個大型項目,需要付出巨大的努力才能遷移整個代碼庫。另一個原因是大家對 React 類已經很熟悉了。有足夠經驗的話,繼續使用類會感到更自在。