寫React Hooks前必讀

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

  1. 寫仍是要寫的;
  2. 寫hooks前必定要先學習hooks;
  3. 團隊再出一篇必讀文檔,必需要求每位同窗,先讀再寫。

所以便有了此文。html

本文主要講兩大點:前端

  1. 寫hooks前的硬性要求;
  2. 寫hooks常見的幾個注意點。

硬性要求

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

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

另外推薦閱讀:react

  1. Dan的《useEffect徹底指南》
  2. 衍良同窗的《React Hooks徹底上手指南

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

lint插件:www.npmjs.com/package/esl…
必開規則:npm

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

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

切記,本條是硬性條件。api

若是你的工程,當前沒開啓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瘋狂的執行。真實開發的話,你們應該會常常遇到這種問題。

因此,在此建議:

  1. 在組件內部,那些會成爲其餘useEffect依賴項的方法,建議用 useCallback 包裹,或者直接編寫在引用它的useEffect中。
  2. 己所不欲勿施於人,若是你的function會做爲props傳遞給子組件,請必定要使用 useCallback 包裹,對於子組件來講,若是每次render都會致使你傳遞的函數發生變化,可能會對它形成很是大的困擾。同時也不利於react作渲染優化。

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

舉個例子:

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

咱們但願每2秒只能觸發一次 count + 1 ,這個組件在理想邏輯下是OK的。但現實是骨感的,咱們的頁面組件很是多,這個 BadDemo 可能因爲父級什麼操做就從新render了。如今假使咱們頁面每500毫秒會從新render一次,那麼就是這樣:

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 2000);
  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);
}, 2000), []);
複製代碼

如今咱們發現效果知足咱們指望了,但這背後還藏着一個驚天大坑。
假如說,這個防抖的函數有一些依賴呢?好比 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 。

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

相關文章
相關標籤/搜索