Try   HackMD

React 優化項目(二): useCallback

tags: React Optimize Hooks useCallback OptimizeRender

React 優化項目系列,減少大量且不必要的元件重新渲染週期

React 優化項目(一):React.memo

如何觸發更新的
紀錄 props value

React 優化項目(二):useCallback

紀錄 func 記憶體位置

React 優化項目(三):useMemo

紀錄 object value 避免重複執行相關處理


延續上一篇文章範例,把 Button 包成一個元件傳進一個 event function,加上 log 以及 React.memo 測試。

新增 Button 元件

import React from 'react';
function Button(props) {
  console.log('Btn RUNNING!');
  return <button onClick={props.onClick}>Toggle paragraph</button>;
}
// 加上 memo
export default React.memo(Button);

RenderTry 引用

...
import Button from '../../components/Button';
export default function RenderTry() {
  ...
    <Button onClick={toggleParagraph}>Toggle paragraph</Button>
  ...

瀏覽器執行時可以發現,就算 Button 元件加上了 React.memo,但還是會進行更新。 這是因為 javascript 傳值與傳址的特性

false === false // true
'abc' === 'abc' // true
[1,2,3] === [1,2,3] // flase

當父層元件更新時 toggleParagraphHandler func 重新創造了一次(re-creation),產生了新的記憶體位置。所以對 React.memo 來說,這個 func 是不一樣的


使用 React.useCallback()

使用React.useCallback 紀錄 func 記憶體位置,避免 re-creation

可以先了解基本用法 >>

父層元件 RenderTry 修改

為了避免 toggleParagraphHandler func re-creation ,加上 useCallback

import React, { useState, useCallback } from 'react';
import DemoOutput from '../../components/demoOutput';
import Button from '../../components/Button';

export default function RenderTry() {
  const [showParagraph, setShowParagraph] = useState(false);
  console.log('APP RUNNING', showParagraph);

  // 加上 useCallback
  const toggleParagraph = useCallback(() => {
    setShowParagraph((pre) => !pre);
  }, []);

  return (
    <div>
      <h1>Hi There!</h1>
      {/* 引用 DemoOutput 並傳入 props */}
      <DemoOutput show={false} />
      <Button onClick={toggleParagraph}>Toggle paragraph</Button>
    </div>
  );
}

很棒! Btn RUNNING 不見了


繼續探討 useCallback 的 Dependencies

  • js function 特性是封閉性的 > 閉包 closures,會將內部的值鎖定再 func 內

新增修改 RenderTry

  • 加上 allowToggle state
  • 加上新的 btn 去設定 allowToggle
  • 預期 allowToggle 為 true 時,就能夠切換 toggleParagraph
import React, { useState, useCallback } from 'react';
import DemoOutput from '../../components/demoOutput';
import Button from '../../components/Button';

export default function RenderTry() {
  const [showParagraph, setShowParagraph] = useState(false);
  
  // 新增 allowToggle
  const [allowToggle, setAllowToggle] = useState(false);
  console.log('APP RUNNING', showParagraph);

  // 新增判斷 allowToggle
  const toggleParagraph = useCallback(() => {
    allowToggle && setShowParagraph((pre) => !pre);
  }, []);

  // 新增控制 allowToggle
  const allowToggleHandle = () => {
    setAllowToggle(true);
  };

  return (
    <div>
      <h1>Hi There!</h1>
      <DemoOutput show={false} />
      <Button onClick={toggleParagraph}>Toggle paragraph</Button>
      
      {/* 新增 event btn */}
      <Button onClick={allowToggleHandle}>allowToggle</Button>
    </div>
  );
}

範例執行時可以發現,當我點擊 allowToggleHandle 更改 allowToggle 為 true 時,再點擊 toggleParagraph 是無效的

這就是 js 閉包的特性,他已經把 allowToggle false 記住了,更新時他卻不知道。 為了通知 toggleParagraph 需要更新可以在 useCallbackdependencies 加上 allowToggle 這樣一來當 useCallback 就會去偵測 allowToggle 是否有改變,有的話就更新 toggleParagraph

...
  const toggleParagraph = useCallback(() => {
    allowToggle && setShowParagraph((pre) => !pre);
  }, [allowToggle]);
  // ^^ 加入 allowToggle ^^
  ...

重新執行,一切正常,真棒~