學習 React Hooks 可能會遇到的五個靈魂問題

  • 2019 年 11 月 8 日
  • 筆記

作者:橘子小睿 原文鏈接:https://zhuanlan.zhihu.com/p/85969406

之前寫新手學習 react 迷惑的點(完整版)反響還不錯,很多讀者要求寫一篇 React Hooks 相關的,最近正好在知乎上看到一篇關於可能在使用 hooks 的疑問,我覺得寫得很棒,所以找作者橘子小睿拿到授權,分享給大家,下面是正文:


正文

從 React Hooks 正式發布到現在,我一直在項目使用它。但是,在使用 Hooks 的過程中,我也進入了一些誤區,導致寫出來的程式碼隱藏 bug 並且難以維護。這篇文章中,我會具體分析這些問題,並總結一些好的實踐,以供大家參考。

問題一:我該使用單個 state 變數還是多個 state 變數?

useState 的出現,讓我們可以使用多個 state 變數來保存 state,比如:

const [width, setWidth] = useState(100);  const [height, setHeight] = useState(100);  const [left, setLeft] = useState(0);  const [top, setTop] = useState(0);

但同時,我們也可以像 Class 組件的 this.state 一樣,將所有的 state 放到一個 object 中,這樣只需一個 state 變數即可:

const [state, setState] = useState({    width: 100,    height: 100,    left: 0,    top: 0  });

那麼問題來了,到底該用單個 state 變數還是多個 state 變數呢?

如果使用單個 state 變數,每次更新 state 時需要合併之前的 state。因為 useState 返回的 setState 會替換原來的值。這一點和 Class 組件的 this.setState 不同。this.setState 會把更新的欄位自動合併到 this.state 對象中。

const handleMouseMove = (e) => {    setState((prevState) => ({      ...prevState,      left: e.pageX,      top: e.pageY,    }))  };

使用多個 state 變數可以讓 state 的粒度更細,更易於邏輯的拆分和組合。比如,我們可以將關聯的邏輯提取到自定義 Hook 中:

function usePosition() {    const [left, setLeft] = useState(0);    const [top, setTop] = useState(0);      useEffect(() => {      // ...    }, []);      return [left, top, setLeft, setTop];  }

我們發現,每次更新 lefttop 也會隨之更新。因此,把 topleft 拆分為兩個 state 變數顯得有點多餘。

在使用 state 之前,我們需要考慮狀態拆分的「粒度」問題。如果粒度過細,程式碼就會變得比較冗餘。如果粒度過粗,程式碼的可復用性就會降低。那麼,到底哪些 state 應該合併,哪些 state 應該拆分呢?我總結了下面兩點:

  1. 將完全不相關的 state 拆分為多組 state。比如 sizeposition
  2. 如果某些 state 是相互關聯的,或者需要一起發生改變,就可以把它們合併為一組 state。比如 lefttop
function Box() {    const [position, setPosition] = usePosition();    const [size, setSize] = useState({width: 100, height: 100});    // ...  }    function usePosition() {    const [position, setPosition] = useState({left: 0, top: 0});      useEffect(() => {      // ...    }, []);      return [position, setPosition];  }

問題二:deps 依賴過多,導致 Hooks 難以維護?

使用 useEffect hook 時,為了避免每次 render 都去執行它的 callback,我們通常會傳入第二個參數「dependency array」(下面統稱為依賴數組)。這樣,只有當依賴數組發生變化時,才會執行 useEffect 的回調函數。

function Example({id, name}) {    useEffect(() => {      console.log(id, name);    }, [id, name]);  }

在上面的例子中,只有當 idname 發生變化時,才會列印日誌。依賴數組中必須包含在 callback 內部用到的所有參與 React 數據流的值,比如 stateprops 以及它們的衍生物。如果有遺漏,可能會造成 bug。這其實就是 JS 閉包問題,對閉包不清楚的同學可以自行 google,這裡就不展開了。

function Example({id, name}) {    useEffect(() => {      // 由於依賴數組中不包含 name,所以當 name 發生變化時,無法列印日誌      console.log(id, name);    }, [id]);  }

在 React 中,除了 useEffect 外,接收依賴數組作為參數的 Hook 還有 useMemouseCallbackuseImperativeHandle。我們剛剛也提到了,依賴數組中千萬不要遺漏回調函數內部依賴的值。但是,如果依賴數組依賴了過多東西,可能導致程式碼難以維護。我在項目中就看到了這樣一段程式碼:

const refresh = useCallback(() => {    // ...  }, [name, searchState, address, status, personA, personB, progress, page, siz

不要說內部邏輯了,光是看到這一堆依賴就令人頭大!如果項目中到處都是這樣的程式碼,可想而知維護起來多麼痛苦。如何才能避免寫出這樣的程式碼呢?

首先,你需要重新思考一下,這些 deps 是否真的都需要?看下面這個例子:

function Example({id}) {    const requestParams = useRef({});    requestParams.current = {page: 1, size: 20, id};      const refresh = useCallback(() => {      doRefresh(requestParams.current);    }, []);        useEffect(() => {      id && refresh();    }, [id, refresh]); // 思考這裡的 deps list 是否合理?  }

雖然 useEffect 的回調函數依賴了 idrefresh 方法,但是觀察 refresh 方法可以發現,它在首次 render 被創建之後,永遠不會發生改變了。因此,把它作為 useEffect 的 deps 是多餘的。

其次,如果這些依賴真的都是需要的,那麼這些邏輯是否應該放到同一個 hook 中?

function Example({id, name, address, status, personA, personB, progress}) {    const [page, setPage] = useState();    const [size, setSize] = useState();      const doSearch = useCallback(() => {      // ...    }, []);      const doRefresh = useCallback(() => {      // ...    }, []);        useEffect(() => {      id && doSearch({name, address, status, personA, personB, progress});      page && doRefresh({name, page, size});    }, [id, name, address, status, personA, personB, progress, page, size]);  }

可以看出,在 useEffect 中有兩段邏輯,這兩段邏輯是相互獨立的,因此我們可以將這兩段邏輯放到不同 useEffect 中:

useEffect(() => {    id && doSearch({name, address, status, personA, personB, progress});  }, [id, name, address, status, personA, personB, progress]);    useEffect(() => {    page && doRefresh({name, page, size});  }, [name,  page, size]);

如果邏輯無法繼續拆分,但是依賴數組還是依賴了過多東西,該怎麼辦呢?就比如我們上面的程式碼:

useEffect(() => {    id && doSearch({name, address, status, personA, personB, progress});  }, [id, name, address, status, personA, personB, progress]);

這段程式碼中的 useEffect 依賴了七個值,還是偏多了。仔細觀察上面的程式碼,可以發現這些值都是「過濾條件」的一部分,通過這些條件可以過濾頁面上的數據。因此,我們可以將它們看做一個整體,也就是我們前面講過的合併 state:

const [filters, setFilters] = useState({    name: "",    address: "",    status: "",    personA: "",    personB: "",    progress: ""  });    useEffect(() => {    id && doSearch(filters);  }, [id, filters]);

如果 state 不能合併,在 callback 內部又使用了 setState 方法,那麼可以考慮使用 setState callback 來減少一些依賴。比如:

const useValues = () => {    const [values, setValues] = useState({      data: {},      count: 0    });      const [updateData] = useCallback(        (nextData) => {          setValues({            data: nextData,            count: values.count + 1 // 因為 callback 內部依賴了外部的 values 變數,所以必須在依賴數組中指定它          });        },        [values],    );      return [values, updateData];  };

上面的程式碼中,我們必須在 useCallback 的依賴數組中指定 values,否則我們無法在 callback 中獲取到最新的 values 狀態。但是,通過 setState 回調函數,我們不用再依賴外部的 values 變數,因此也無需在依賴數組中指定它。就像下面這樣:

const useValues = () => {    const [values, setValues] = useState({});      const [updateData] = useCallback((nextData) => {      setValues((prevValues) => ({        data: nextData,        count: prevValues.count + 1, // 通過 setState 回調函數獲取最新的 values 狀態,這時 callback 不再依賴於外部的 values 變數了,因此依賴數組中不需要指定任何值      }));    }, []); // 這個 callback 永遠不會重新創建      return [values, updateData];  };

最後,還可以通過 ref 來保存可變變數。以前我們只把 ref 用作保持 DOM 節點引用的工具,可 useRef Hook 能做的事情遠不止如此。我們可以用它來保存一些值的引用,並對它進行讀寫。舉個例子:

const useValues = () => {    const [values, setValues] = useState({});    const latestValues = useRef(values);    latestValues.current = values;      const [updateData] = useCallback((nextData) => {      setValues({        data: nextData,        count: latestValues.current.count + 1,      });    }, []);      return [values, updateData];  };

在使用 ref 時要特別小心,因為它可以隨意賦值,所以一定要控制好修改它的方法。特別是一些底層模組,在封裝的時候千萬不要直接暴露 ref,而是提供一些修改它的方法。

說了這麼多,歸根到底都是為了寫出更加清晰、易於維護的程式碼。如果發現依賴數組依賴過多,我們就需要重新審視自己的程式碼。

  • 依賴數組依賴的值最好不要超過 3 個,否則會導致程式碼會難以維護。
  • 如果發現依賴數組依賴的值過多,我們應該採取一些方法來減少它。
  • 去掉不必要的依賴。
  • 將 Hook 拆分為更小的單元,每個 Hook 依賴於各自的依賴數組。
  • 通過合併相關的 state,將多個依賴值聚合為一個。
  • 通過 setState 回調函數獲取最新的 state,以減少外部依賴。
  • 通過 ref 來讀取可變變數的值,不過需要注意控制修改它的途徑。

問題三:該不該使用 useMemo

該不該使用 useMemo?對於這個問題,有的人從來沒有思考過,有的人甚至不覺得這是個問題。不管什麼情況,只要用 useMemo 或者 useCallback 「包裹一下」,似乎就能使應用遠離性能的問題。但真的是這樣嗎?有的時候 useMemo 沒有任何作用,甚至還會影響應用的性能。

為什麼這麼說呢?首先,我們需要知道 useMemo本身也有開銷。useMemo 會「記住」一些值,同時在後續 render 時,將依賴數組中的值取出來和上一次記錄的值進行比較,如果不相等才會重新執行回調函數,否則直接返回「記住」的值。這個過程本身就會消耗一定的記憶體和計算資源。因此,過度使用 useMemo 可能會影響程式的性能。

要想合理使用 useMemo,我們需要搞清楚 useMemo 適用的場景:

  • 有些計算開銷很大,我們就需要「記住」它的返回值,避免每次 render 都去重新計算。
  • 由於值的引用發生變化,導致下游組件重新渲染,我們也需要「記住」這個值。

讓我們來看個例子:

interface IExampleProps {    page: number;    type: string;  }    const Example = ({page, type}: IExampleProps) => {    const resolvedValue = useMemo(() => {      return getResolvedValue(page, type);    }, [page, type]);      return <ExpensiveComponent resolvedValue={resolvedValue}/>;  };

在上面的例子中,渲染 ExpensiveComponent 的開銷很大。所以,當 resolvedValue 的引用發生變化時,作者不想重新渲染這個組件。因此,作者使用了 useMemo,避免每次 render 重新計算 resolvedValue,導致它的引用發生改變,從而使下游組件 re-render。

這個擔憂是正確的,但是使用 useMemo 之前,我們應該先思考兩個問題:

  1. 傳遞給 useMemo 的函數開銷大不大?在上面的例子中,就是考慮 getResolvedValue 函數的開銷大不大。JS 中大多數方法都是優化過的,比如 Array.mapArray.forEach 等。如果你執行的操作開銷不大,那麼就不需要記住返回值。否則,使用 useMemo 本身的開銷就可能超過重新計算這個值的開銷。因此,對於一些簡單的 JS 運算來說,我們不需要使用 useMemo 來「記住」它的返回值。
  2. 當輸入相同時,「記憶」值的引用是否會發生改變?在上面的例子中,就是當 pagetype 相同時,resolvedValue 的引用是否會發生改變?這裡我們就需要考慮 resolvedValue 的類型了。如果 resolvedValue 是一個對象,由於我們項目上使用「函數式編程」,每次函數調用都會產生一個新的引用。但是,如果 resolvedValue 是一個原始值(string, boolean, null, undefined, number, symbol),也就不存在「引用」的概念了,每次計算出來的這個值一定是相等的。也就是說,ExpensiveComponent 組件不會被重新渲染。

因此,如果 getResolvedValue 的開銷不大,並且 resolvedValue 返回一個字元串之類的原始值,那我們完全可以去掉 useMemo,就像下面這樣:

interface IExampleProps {    page: number;    type: string;  }    const Example = ({page, type}: IExampleProps) => {    const resolvedValue = getResolvedValue(page, type);    return <ExpensiveComponent resolvedValue={resolvedValue}/>;  };

還有一個誤區就是對創建函數開銷的評估。有的人覺得在 render 中創建函數可能會開銷比較大,為了避免函數多次創建,使用了 useMemo 或者 useCallback。但是對於現代瀏覽器來說,創建函數的成本微乎其微。因此,我們沒有必要使用 useMemo 或者 useCallback 去節省這部分性能開銷。當然,如果是為了保證每次 render 時回調的引用相等,你可以放心使用 useMemo 或者 useCallback

const Example = () => {    const onSubmit = useCallback(() => { // 考慮這裡的 useCallback 是否必要?      doSomething();    }, []);      return <form onSubmit={onSubmit}></form>;  };

我之前看過一篇文章(鏈接在文章的最後),這篇文章中提到,如果只是想在重新渲染時保持值的引用不變,更好的方法是使用 useRef,而不是 useMemo。我並不同意這個觀點。讓我們來看個例子:

// 使用 useMemo  function Example() {    const users = useMemo(() => [1, 2, 3], []);      return <ExpensiveComponent users={users} />  }    // 使用 useRef  function Example() {    const {current: users} = useRef([1, 2, 3]);      return <ExpensiveComponent users={users} />  }

在上面的例子中,我們用 useMemo 來「記住」users 數組,不是因為數組本身的開銷大,而是因為 users 的引用在每次 render 時都會發生改變,從而導致子組件 ExpensiveComponent 重新渲染(可能會帶來較大開銷)。

作者認為從語義上不應該使用 useMemo,而是應該使用 useRef,否則會消耗更多的記憶體和計算資源。雖然在 React 中 useRefuseMemo 的實現有一點差別,但是當 useMemo 的依賴數組為空數組時,它和 useRef 的開銷可以說相差無幾。useRef 甚至可以直接用 useMemo 來實現,就像下面這樣:

const useRef = (v) => {    return useMemo(() => ({current: v}), []);  };

因此,我認為使用 useMemo 來保持值的引用一致沒有太大問題。

在編寫自定義 Hook 時,返回值一定要保持引用的一致性。因為你無法確定外部要如何使用它的返回值。如果返回值被用做其他 Hook 的依賴,並且每次 re-render 時引用不一致(當值相等的情況),就可能會產生 bug。比如:

function Example() {    const data = useData();    const [dataChanged, setDataChanged] = useState(false);      useEffect(() => {      setDataChanged((prevDataChanged) => !prevDataChanged); // 當 data 發生變化時,調用 setState。如果 data 值相同而引用不同,就可能會產生非預期的結果。    }, [data]);      console.log(dataChanged);      return <ExpensiveComponent data={data} />;  }    const useData = () => {    // 獲取非同步數據    const resp = getAsyncData([]);      // 處理獲取到的非同步數據,這裡使用了 Array.map。因此,即使 data 相同,每次調用得到的引用也是不同的。    const mapper = (data) => data.map((item) => ({...item, selected: false}));      return resp ? mapper(resp) : resp;  };

在上面的例子中,我們通過 useData Hook 獲取了 data。每次 render 時 data 的值沒有發生變化,但是引用卻不一致。如果把 data 用到 useEffect 的依賴數組中,就可能產生非預期的結果。另外,由於引用的不同,也會導致 ExpensiveComponent 組件 re-render,產生性能問題。

如果因為 prop 的值相同而引用不同,從而導致子組件發生 re-render,不一定會造成性能問題。因為 Virtual DOM re-render ≠ DOM re-render。但是當子組件特別大時,Virtual DOM 的 Diff 開銷也很大。因此,還是應該盡量避免子組件 re-render。

因此,在使用 useMemo 之前,我們不妨先問自己幾個問題:

  1. 要記住的函數開銷很大嗎?
  2. 返回的值是原始值嗎?
  3. 記憶的值會被其他 Hook 或者子組件用到嗎?

回答出上面這幾個問題,判斷是否應該使用 useMemo 也就不再困難了。不過在實際項目中,還是最好定義出一套統一的規範,方便團隊中多人協作。比如第一個問題,開銷很大如何定義?如果沒有明確的標準,執行起來會非常困難。因此,我總結了下面幾條規則:

  1. 如果返回的值是原始值:string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),則不需要使用 useMemo
  2. 對於組件內部用到的 object、array、函數等,如果沒有用到其他 Hook 的依賴數組中,或者造成子組件 re-render,可以不使用 useMemo
  3. 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。

問題四:Hooks 能替代高階組件和 Render Props 嗎?

在 Hooks 出現之前,我們有兩種方法可以復用組件邏輯:Render Props[1]高階組件[2]。但是這兩種方法都可能會造成 JSX「嵌套地域」的問題。Hooks 的出現,讓組件邏輯的復用變得更簡單,同時解決了「嵌套地域」的問題。Hooks 之於 React 就像 async / await 之於 Promise 一樣。

那 Hooks 能替代高階組件和 Render Props 嗎?官方給出的回答是,在高階組件或者 Render Props 只渲染一個子組件時,Hook 提供了一種更簡單的方式。不過在我看來,Hooks 並不能完全替代 Render Props 和高階組件。接下來,我們會詳細分析這個問題。

高階組件 HOC

高階組件是一個函數,它接受一個組件作為參數,返回一個新的組件。

function enhance(Comp) {    // 增加一些其他的功能    return class extends Component {      // ...      render() {        return <Comp />;      }    };  }

高階組件採用了裝飾器模式,讓我們可以增強原有組件的功能,並且不破壞它原有的特性。例如:

const RedButton = withStyles({    root: {      background: "red",    },  })(Button);

在上面的程式碼中,我們希望保留 Button 組件的邏輯,但同時我們又想使用它原有的樣式。因此,我們通過 withStyles 這個高階組件注入了自定義的樣式,並且生成了一個新的組件 RedButton

Render Props

Render Props 通過父組件將可復用邏輯封裝起來,並把數據提供給子組件。至於子組件拿到數據之後要怎麼渲染,完全由子組件自己決定,靈活性非常高。而高階組件中,渲染結果是由父組件決定的。Render Props 不會產生新的組件,而且更加直觀的體現了「父子關係」。

<Parent>    {(data) => {      // 你父親已經把江山給你打好了,並給你留下了一堆金幣,至於怎麼花就看你自己了      return <Child data={data} />;    }}  </Parent>

Render Props 作為 JSX 的一部分,可以很方便地利用 React 生命周期和 Props、State 來進行渲染,在渲染上有著非常高的自由度。同時,它不像 Hooks 需要遵守一些規則,你可以放心大膽的在它裡面使用 if / else、map 等各類操作。

在大部分情況下,高階組件和 Render Props 是可以相互轉換的,也就是說用高階組件能實現的,用 Render Props 也能實現。只不過在不同的場景下,哪種方式使用起來簡單一點罷了。

將上面 HOC 的例子改成 Render Props,使用起來確實要「麻煩」一點:

<RedButton>    {(styles)=>(      <Button styles={styles}/>    )}  </RedButton>

小結

沒有 Hooks 之前,高階組件和 Render Props 本質上都是將復用邏輯提升到父組件中。而 Hooks 出現之後,我們將復用邏輯提取到組件頂層,而不是強行提升到父組件中。這樣就能夠避免 HOC 和 Render Props 帶來的「嵌套地域」。但是,像 Context 的 <Provider/><Consumer/> 這樣有父子層級關係(樹狀結構關係)的,還是只能使用 Render Props 或者 HOC。

對於 Hooks、Render Props 和高階組件來說,它們都有各自的使用場景:

  • Hooks:
  • 替代 Class 的大部分用例,除了 getSnapshotBeforeUpdatecomponentDidCatch 還不支援。
  • 提取復用邏輯。除了有明確父子關係的,其他場景都可以使用 Hooks。
  • Render Props:在組件渲染上擁有更高的自由度,可以根據父組件提供的數據進行動態渲染。適合有明確父子關係的場景。
  • 高階組件:適合用來做注入,並且生成一個新的可復用組件。適合用來寫插件。

不過,能使用 Hooks 的場景還是應該優先使用 Hooks,其次才是 Render Props 和 HOC。當然,Hooks、Render Props 和 HOC 不是對立的關係。我們既可以用 Hook 來寫 Render Props 和 HOC,也可以在 HOC 中使用 Render Props 和 Hooks。

問題五:使用 Hooks 時還有哪些好的實踐?

  1. 若 Hook 類型相同,且依賴數組一致時,應該合併成一個 Hook。否則會產生更多開銷。
const dataA = useMemo(() => {    return getDataA();  }, [A, B]);    const dataB = useMemo(() => {    return getDataB();  }, [A, B]);    // 應該合併為    const [dataA, dataB] = useMemo(() => {    return [getDataA(), getDataB()]  }, [A, B]);
  1. 參考原生 Hooks 的設計,自定義 Hooks 的返回值可以使用 Tuple 類型,更易於在外部重命名。但如果返回值的數量超過三個,還是建議返回一個對象。
export const useToggle = (defaultVisible: boolean = false) => {    const [visible, setVisible] = useState(defaultVisible);    const show = () => setVisible(true);    const hide = () => setVisible(false);      return [visible, show, hide] as [typeof visible, typeof show, typeof hide];  };    const [isOpen, open, close] = useToggle(); // 在外部可以更方便地修改名字  const [visible, show, hide] = useToggle();
  1. ref 不要直接暴露給外部使用,而是提供一個修改值的方法。
  2. 在使用 useMemo 或者 useCallback 時,確保返回的函數只創建一次。也就是說,函數不會根據依賴數組的變化而二次創建。舉個例子:
export const useCount = () => {    const [count, setCount] = useState(0);      const [increase, decrease] = useMemo(() => {      const increase = () => {        setCount(count + 1);      };        const decrease = () => {        setCount(count - 1);      };      return [increase, decrease];    }, [count]);      return [count, increase, decrease];  };

useCount Hook 中, count 狀態的改變會讓 useMemo 中的 increasedecrease 函數被重新創建。由於閉包特性,如果這兩個函數被其他 Hook 用到了,我們應該將這兩個函數也添加到相應 Hook 的依賴數組中,否則就會產生 bug。比如:

function Counter() {    const [count, increase] = useCount();      useEffect(() => {      const handleClick = () => {        increase(); // 執行後 count 的值永遠都是 1      };        document.body.addEventListener("click", handleClick);      return () => {        document.body.removeEventListener("click", handleClick);      };    }, []);      return <h1>{count}</h1>;  }

useCount 中,increase 會隨著 count 的變化而被重新創建。但是 increase 被重新創建之後, useEffect 並不會再次執行,所以 useEffect 中取到的 increase 永遠都是首次創建時的 increase 。而首次創建時 count 的值為 0,因此無論點擊多少次, count 的值永遠都是 1。

那把 increase 函數放到 useEffect 的依賴數組中不就好了嗎?事實上,這會帶來更多問題:

  • increase 的變化會導致頻繁地綁定事件監聽,以及解除事件監聽。
  • 需求是只在組件 mount 時執行一次 useEffect,但是 increase 的變化會導致 useEffect 多次執行,不能滿足需求。

如何解決這些問題呢?

一、通過 setState 回調,讓函數不依賴外部變數。例如:

export const useCount = () => {    const [count, setCount] = useState(0);      const [increase, decrease] = useMemo(() => {      const increase = () => {        setCount((latestCount) => latestCount + 1);      };        const decrease = () => {        setCount((latestCount) => latestCount - 1);      };      return [increase, decrease];    }, []); // 保持依賴數組為空,這樣 increase 和 decrease 方法都只會被創建一次      return [count, increase, decrease];  };

二、通過 ref 來保存可變變數。例如:

export const useCount = () => {    const [count, setCount] = useState(0);    const countRef = useRef(count);    countRef.current = count;      const [increase, decrease] = useMemo(() => {      const increase = () => {        setCount(countRef.current + 1);      };        const decrease = () => {        setCount(countRef.current - 1);      };      return [increase, decrease];    }, []); // 保持依賴數組為空,這樣 increase 和 decrease 方法都只會被創建一次      return [count, increase, decrease];  };

最後

我們總結了在實踐中一些常見的問題,並提出了一些解決方案。最後讓我們再來回顧一下:

  1. 將完全不相關的 state 拆分為多組 state。
  2. 如果某些 state 是相互關聯的,或者需要一起發生改變,就可以把它們合併為一組 state。
  3. 依賴數組依賴的值最好不要超過 3 個,否則會導致程式碼會難以維護。
  4. 如果發現依賴數組依賴的值過多,我們應該採取一些方法來減少它。
  5. 去掉不必要的依賴。
  6. 將 Hook 拆分為更小的單元,每個 Hook 依賴於各自的依賴數組。
  7. 通過合併相關的 state,將多個依賴值聚合為一個。
  8. 通過 setState 回調函數獲取最新的 state,以減少外部依賴。
  9. 通過 ref 來讀取可變變數的值,不過需要注意控制修改它的途徑。
  10. 為了確保不濫用 useMemo,我們定義了下面幾條規則:
  11. 如果返回的值是原始值:string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),則不需要使用 useMemo
  12. 對於組件內部用到的 object、array、函數等,如果沒有用到其他 Hook 的依賴數組中,或者造成子組件 re-render,可以不使用 useMemo
  13. 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。
  14. Hooks、Render Props 和高階組件都有各自的使用場景,具體使用哪一種要看實際情況。
  15. 若 Hook 類型相同,且依賴數組一致時,應該合併成一個 Hook。
  16. 自定義 Hooks 的返回值可以使用 Tuple 類型,更易於在外部重命名。如果返回的值過多,則不建議使用。
  17. ref 不要直接暴露給外部使用,而是提供一個修改值的方法。
  18. 在使用 useMemo 或者 useCallback 時,可以藉助 ref 或者 setState callback,確保返回的函數只創建一次。也就是說,函數不會根據依賴數組的變化而二次創建。

參考文章:

You』re overusing useMemo: Rethinking Hooks memoization[3]

參考資料

[1]

Render Props: https://link.zhihu.com/?target=https%3A//reactjs.org/docs/render-props.html

[2]

高階組件: https://link.zhihu.com/?target=https%3A//reactjs.org/docs/higher-order-components.html

[3]

You』re overusing useMemo: Rethinking Hooks memoization: https://link.zhihu.com/?target=https%3A//blog.logrocket.com/rethinking-hooks-memoization/%3Ffrom%3Dsinglemessage%26isappinstalled%3D0