React Hooks 帶來的困擾與思考

當前使用 React 版本:v16.10.2react

自從 React Hooks 推出以來,給平常工做帶來新的開發體驗,提升開發效率的同時也增長了代碼的可閱讀性。本人最近恰好接到一個從零開始的項目需求,趁這個機會就採用 Hooks 函數式組件進行開發,目前代碼量已有1w+。相較於以前的 class 組件,Hooks 下的函數式組件確實帶來了很多便利性,讓我這個從 Vue 轉過來的從新喜歡上了 React。好處之類的就不詳細說了,各大文章也有介紹,本文主要羅列一下 Hooks 在平常開發中遇到一些糾結的地方。git

依賴數組的正確性問題

在官方文檔中,一直強調要確保 useEffect 依賴數組的正確性,反作用的函數中,使用了 props 或 state 變量必定要存在於依賴數組中。使用了官方提供的 eslint 配置後,反作用函數中有 props 或 state 變量未存在於依賴數組中,會有 exhaustive-deps 規則的提醒。然而在某些場景下,這又與這個規則不符。github

好比:但願 useEffect 只關心部分 props 或 state 變量的變更,從而從新執行反作用函數,其它 props 或 state 變量只取決於當時的狀態。npm

場景1:只在組件 mount 以後執行的方法。數組

例子中但願實現 componentDidMount 相似的功能,可是由於引用了 count 變量且未在 useEffect 的依賴數組中聲明,就觸發了 exhaustive-deps 規則的提醒。出於對 eslint 規則的遵照(強迫症),在不由用該規則的前提下,只好經過 useRef 來避開這個規則。對反作用函數中每一個 props 或 state 變量都建立對應的 useRef 值顯然比較麻煩且不合理,那就拿反作用函數來操做好了。爲了便於複用,封裝成 useMount 函數,也可以使用 react-use 庫。bash

function useMount(mountedFn) {
  const mountedFnRef = useRef(null);

  mountedFnRef.current = mountedFn;

  useEffect(() => {
    mountedFnRef.current();
  }, [mountedFnRef]);
}
複製代碼

使用後,warning 解除。閉包

場景2:只關心部分變量的變更。函數

例子中的 Modal 組件須要根據 visible 變量的變更來執行相應的方法,又須要引用到其它的 props 或 state 變量,可是又不但願將它們放入 useEffect 依賴數組裏,由於不關心它們的變更。若是將它們放入 useEffect 數組中,在 visible 變量不變的狀況下,其它變量的變更會帶來反作用函數的重複執行,這多是非預期的。這時就須要一個輔助變量來記錄 visible 變量的前一狀態值,用來在反作用函數中判斷是否由於 visible 變量變更觸發的函數執行。爲了便於複用,封裝成 usePrevious 函數,也可以使用 react-use 庫。ui

const usePrevious = (value) => {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
};
複製代碼

使用後,warning 解除。this

從上面的例子能夠看出,咱們但願 useEffect 的依賴數組中是與反作用函數更有效的變量,而不是反作用函數中所有引用的變量。而從官方提供的文檔和 eslint 規則來看,這彷佛與官方傳達的意圖不符。若是要遵循官方更爲嚴格的規則,就須要寫更多的條件判斷。

依賴數組中變量的比較問題

React 對於各 hook 函數依賴數組中變量的比較都是嚴格比較(===),因此咱們須要注意對象類型變量的引用。相似下面的狀況應該儘可能避免,由於每次 App 組件渲染,傳給 Child 組件的 list 變量都是一個全新引用地址的數組。若是 Child 組件將 list 變量放入了某個 hook 函數的依賴數組裏,就會引發該 hook 函數的依賴變更。

function App() {
  const list = [1, 2, 3];

  return (
    <>
      <Child list={list}></Child>
      <Child list={[4, 5, 6]}></Child>
    </>
  );
}
複製代碼

上面這種狀況多加註意仍是能夠避免的,但在某些狀況下咱們但願依賴數組中對象類型的比較是淺比較或深比較。在 componnetDidUpdate 聲明周期函數中這確實不難實現,但在函數式組件中仍是須要藉助 useRef 函數。

例子:

import { isEqual } from 'lodash';

function useCampare(value, compare) {
  const ref = useRef(null);

  if (!compare(value, ref.current)) {
    ref.current = value;
  }

  return ref.current;
}

function Child({ list }) {
  const listArr = useCampare(list, isEqual);

  useEffect(() => {
    console.log(listArr);
  }, [listArr]);
}
複製代碼

在該例子中,使用了一個 ref 變量,每次組件渲染時都會取以前的值與當前值進行自定義函數的比較,若是不相同,則覆蓋當前值,最後返回 ref.current 值。從而實現了自定義依賴數組比較方法的功能。

函數引用變更問題

在 class 組件中,傳遞給子組件的 props 變量中函數大多數都是 this.xxx 形式,即引用都是穩定的。爲了讓函數能保持引用穩定,React 提供了 useCallback 函數,只要依賴數組保持穩定,會返回一個引用穩定的函數。若是要嚴格遵循該要求,useCallback 可能會成整個項目中最經常使用的 API。但當咱們耗費心思去保持函數的引用穩定,可是組件樹上層一個不當心可能就將以前的努力白費。

好比:

function Button({
  child,
  disabled,
  onClick
}) {
  /**
   * 備註:
   * 如何不將 disabled 和 onClick  變量加入依賴數組中,
   * handleBtnClick 函數觸發時,只會取得第一次渲染的值。
   */
  const handleBtnClick = useCallback(() => {
    if (!disabled && onClick) {
      onClick();
    }
  }, [disabled, onClick]);

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}

function App() {
  const onBtnClick = () => {};

  return (
    <Button onClick={onBtnClick} />
  );
}
複製代碼

上面例子中,咱們但願 Button 組件中 handleBtnClick 函數只在 disabled 和 onClick 變量的引用變更時才返回一個新的引用的函數。但在 App 組件中卻沒有使用 useCallback 來保持 onBtnClick 函數的引用穩定,從而每次渲染傳遞給 Button 組件 都是全新引用的函數。子組件接收到這個全新引用的函數,就會觸發 useCallback 的依賴變更從新生成一個全新引用的 handleBtnClick 函數。

並且在列表渲染中,咱們但願在傳遞給子組件的函數變量中增長值,必然會致使每次父組件渲染,傳遞給子組件的都是全新引用的函數。

function App() {
  const [list] = useState(() => {
    return [1, 2, 3];
  });
  const handleItemClick = useCallback((item) => {
    console.log(item);
  }, []);

  return (
    <div>
      {
        list.map((value) => (
          <Item
            key={value}
            onClick={() => handleItemClick(value)}
          />
        ))
      }
    </div>
  );
}
複製代碼

這樣,咱們在子組件使用 useCallback 反而帶來了沒必要要的比較。其實,對於組件 props 中函數的變量還可使用如下這種形式。

function Button({
  child,
  disabled,
  onClick
}) {
  const handleBtnClickRef = useRef();

  handleBtnClickRef.current = () => {
    if (!disabled && onClick) {
      onClick();
    }
  };

  const handleBtnClick = useCallback(() => {
    handleBtnClickRef.current();
  }, [handleBtnClickRef]);

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}
複製代碼

上面例子中,使用了一個 useRef 函數返回的變量 handleBtnClickRef 來保存最新的函數。由於該變量引用是固定的,因此 handleBtnClick 函數的引用也是固定的,觸發 onClick 回調函數也能拿到最新的 disabled 和 onClick 值。

那麼在平常開發中,到底是使用 useCallback 方案,仍是使用 useCallback + useRef 的 hack 方案呢?這裏還想聽聽各位意見。

useRef 的使用

在整篇文章中,useRef 成了各類問題的解決方案,這種 mutable 的方式實現了相似於 class 的 this 功能。這種方式能夠很好的解決閉包帶來的不方便性,可是有時仍是會糾結該不應都用這種方式。

好比 useEffect 中進行全局事件的綁定:

function App() {
  const handleResize = useCallback(() => {
    console.log(count);  
  }, [count]);
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [handleResize]);
}
複製代碼

在例子中,每次 count 變量的變更都會引發 handleResize 變量的變更,進而引發下方 useEffect 中反作用函數的執行,即執行事件解綁 + 事件綁定。而實際上,咱們只但願在組件掛載時綁定 resize 事件,在組件銷燬時解綁。若是要實現這樣的功能,又須要藉助 useRef。

function App() {
  const handleResizeRef = useRef();
  
  handleResizeRef.current = () => {
    console.log(count);  
  };

  useEffect(() => {
    const handleResize = () => {
        handleResizeRef.current();
    };
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [handleResize]);
}
複製代碼

目前,我不太清楚本身是否太在乎事件的綁定與解綁的次數,又或者說仍用 class 組件生命週期函數來對待 useEffect。固然,從 useEffect 定義來看,是聲明依賴於一系列數據的反作用,這些數據的變更必然須要致使反作用的變更。可是這種聲明式反作用在一些場景下會帶來多餘的代碼執行,好比上面例子中重複的解綁與綁定。

這些都是最近項目實踐中遇到的一些問題點,在此僅作記錄,留待之後想通。

相關文章
相關標籤/搜索