React
08 Dec 2021
9 min read
tags: ReactReact Hooks

[Note] React Hooks 整理(上)

最近開始學習 React Hooks,趁這次機會整理常見的 Hooks,目前尚缺兩個額外 Hooks (useImperativeHandle & useDebugValue),內容慢慢增加中 🏃


文章 Hooks
React Hooks 整理 (上) useState、useEffect、useLayoutEffect、useMemo、useCallback
React Hooks 整理 (下) useRef、useContext、useReducer

Why Hook?

  • 更方便在 component 之間共用 stateful 的邏輯
  • 解決 lifecycle 方法常常將不相關的邏輯混合在一起 e.g. event listener 和抓取資料設置在同個 componentDidMount
  • 降低初學者學習門檻,class 需要記得綁定和事先了解 this 在 JavaScript 中如何運作

Hook 通用規則

  • 只在 React function component 或自定義的 Hook 呼叫 Hook
  • 只在最上層呼叫 Hook:不要在迴圈、判斷式、或是嵌套 function 中呼叫 Hook
// 🚫: 不能放入條件式裡面

const App = () => {
  if (true) {
    const [count, setCount] = useState(0);
  }

  return (
    <h1>React</h1>
  );
};

useState

使用方法

// 語法: const [state, setState] = useState(初始值)

const [count, setCount] = useState(0);

重點

  • 使用陣列解構賦值,回傳的第一個值 count 為當前的 state,第二個值 setCount 為可更新 state 的方法
  • setState 可直接傳值,也可以傳入 function
// 兩種寫法都有相同效果,一樣都會 +1

// 寫法一:直接傳值
const increase = () => {
    setCount(count + 1);
 };

// 寫法二: function 可拿到 previous value
const increase = () => {
    setCount((prevCount)=> prevCount + 1);
};
setState 補充

💡 setState 使用 function 時機:
如果是有使用到 previous value 來設置新的 value 或彼此間有依賴關係。

雖然上面兩個寫法都可達到 +1,但在下面的例子就不適合直接傳值:

increase 內呼叫兩次 setCount,當 increase 被觸發後,我們預期 count 會等於 2,但實際得到 1

因為 setCount(count + 1),裡面的 count 都還是當前 render 值 0,所以等於執行兩次 setCount(0 + 1)

const Count = () => {
  const [count, setCount] = useState(0);

  // 預期為 2,但實際得到 1
  const increase = () => {
    setCount(count + 1); // 此時 count 為 0,相當於 setCount(0 + 1)
    setCount(count + 1); // 此時 count 為 0,相當於 setCount(0 + 1)
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={increase}>+</button>
    </>
  );
};

這時改用 function 寫法, 確保是使用 countprevious value 而不是當前 render 後的值,就會如預期得到 2 :

const Count = () => {
  const [count, setCount] = useState(0);

  const increase = () => {
    setCount(prevCount => prevCount + 1); // 相當於 setCount(0 + 1)
    setCount(prevCount => prevCount + 1); // 相當於 setCount(1 + 1)
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={increase}>+</button>
    </>
  );
};

  • 初始值有兩種表示方式,可直接傳入值,也可以用 function
初始值 補充

💡 初始值使用 function 寫法的時機:

因為每次 render 時候都會重新執行 useSate,如果初始值是需要非常複雜計算時,每次更新需要重新計算,就會降低效能,這時就需要傳入 function,該 function 只會在初始 render 時被調用

// 直接傳值:
const complexCompute = () => {
 console.log("execution");
 return 0;
};

const Count = () => {
 // 每次按 +1 時,都會印出 ‘execution’
 const [count, setCount] = useState(complexCompute());

 const increase = () => {
   setCount(preCount => preCount + 1);
 };

 return (
   <>
     <span>{count}</span>
     <button onClick={increase}>+</button>
   </>
 );
};
// 改用 function
const Count = () => {
  // 首次渲染印出 ‘execution’,後續按 +1 時,不會在印出字了
  const [count, setCount] = useState(() => complexCompute());

  const increase = () => {
    setCount(preCount => preCount + 1);
  };

    ...
}

  • setState 是完全覆蓋 state,而非 merge
// 直接整個覆寫 fruitDate
const Fruit = () => {
  const [fruitDate, setFruitDate] = useState({ amount: 1, fruit: "banana" });

  const change = () => {
    setFruitDate({ fruit: "apple" }); // 觸發後,fruitDate 為 { fruit: "apple" }
  };

  return <button onClick={change}>change fruit</button>
};
// 使用擴展運算子,保留物件內其他屬性
const Fruit = () => {
  const [fruitDate, setFruitDate] = useState({ amount: 1, fruit: "banana" });

  const change = () => {
    setFruitDate({ ...fruitData, fruit: "apple" }); // 觸發後,fruitDate 為 { amount: 1 fruit: "apple" }
  };

  ...
};

useEffect

使用方法

// 語法: useEffect(()=>{},[依賴項])

useEffect(() => {
    // 每次畫面重新渲染後都會執行
    console.log("executed")
});

重點

  • 處理 side effect 事件,e.g. ajax、EventLister
  • DOM 改變 => 渲染畫面 => 調用 useEffect
  • 可以有多個依賴項,皆放在陣列中 useEffect(()=>{ ... },[a, b, c])
  • 搭配第二參數傳入依賴和 return function,可達到原先 React 生命週期 componentDidMountcomponentDidUpdatecomponentWillUnmount 的效果
const Fruit = () => {
  const [fruit, setFruit] = useState("banana");
  const [count, setCount] = useState(0);

  // A. 第二參數不帶入,每次畫面重新渲染,都會執行
  useEffect(() => {
    // 當 fruit 或 count 值改變,畫面重新渲染,都會印出 “changed”
    console.log("changed");
  });


  // B. 第二參數,傳入空陣列 [],只有在第一次渲染時執行
  useEffect(() => {
    // 只在第一次渲染會印出 "init"
    console.log("init");
  }, []);


  // C. 第二參數,傳入依賴項,只有在依賴項改變時才執行
  useEffect(() => {
    // 只有 count 值改變,畫面更新,才會印出 "count changed"
    // 如果是 fruit 值改變則不觸發
    console.log("count changed");
  }, [count]);


  // D. return 的 function 會在組件移除後會觸發
  useEffect(() => {
    // 初次渲染時,建立 setInterval
    const timer = setInterval(() => {
      console.log("hello world");
    }, 1000);

    // 在組件移除後會執行 clearInterval
    return () => clearInterval(timer);
  }, []);

  return (
    <>
      <button onClick={() => setFruit("apple")}>change fruit - {fruit}</button>;
      <button onClick={() => setCount(prev => prev + 1)}>increase count - {count}</button>;
    </>
  );
};

useLayoutEffect

使用方法

// 語法: useLayoutEffect(()=>{},[依賴項])

useLayoutEffect(() => {
     // 每次畫面重新渲染前都會執行
     // 對 DOM 操作 ....
    console.log("executed")
});

重點

  • DOM 改變 => 調用 useLayoutEffect => 渲染畫面
  • 謹慎使用,會影響使用者體驗,因為需要等待 useLayoutEffect 內程式實行完,才會渲染出畫面
  • 使用時機:需要基於 DOM 的 Layout 做額外操作 e.g. 測量 DOM

範例 (useEffect v.s. useLayoutEffect)

- 使用 useEffect

在慢動作的 gif 裡面可以看到一開始的 hi (top: 0) 先出現在按鈕旁邊,在下一個畫面才套用 top: 100px,如果用正常速度看,就會看到第一個 hi 會閃爍一下,這對使用者體驗很不好。

// 使用 useEffect
const Foo = () => {
  const [show, setShow] = useState(false);
  const greetRef = useRef(null);

  useEffect(() => {
    if (greetRef.current === null) return;

    (greetRef.current as HTMLSpanElement).style.top = "100px";
  }, [show]);

  return (
    <>
      <button onClick={() => setShow(prev => !prev)}> toggle Button</button>
      {show && (
        <span ref={greetRef} style={{ position: "absolute" }}>
          hi
        </span>
      )}
    </>
  );
};

useEffect

- 使用 useLayoutEffect

因為在渲染畫面前,先執行完好 hitop: 100px,所以在看到畫面時,已經所設定的位置上了。

// 使用 useLayoutEffect
const Foo = () => {
  const [show, setShow] = useState(false);
  const greetRef = useRef(null);

  useLayoutEffect(() => {
    if (greetRef.current === null) return;

    (greetRef.current as HTMLSpanElement).style.top = "100px";
  }, [show]);

  return (
    <>
      <button onClick={() => setShow(prev => !prev)}> toggle Button</button>
      {show && (
        <span ref={greetRef} style={{ position: "absolute" }}>
          hi
        </span>
      )}
    </>
  );
};

useLayoutEffect

useMemo

使用方法

// 語法: const data = useMemo(()=>{ return 值 },[依賴項])

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 可以將函式返回值 cached(跟 Vue 的 computed 很像),並回傳一個 memoized 的值
  • useMemo 的 function 會在 render 期間執行
  • 可傳入依賴 array,當依賴改變時才重新計算,不提供則每次都計算
  • 需額外記憶體儲存變數,相當於以空間換時間,所以簡單的計算 / 值,要避免濫用
  • 使用情境:當值需要昂貴計算得到,但不需要每次 render 都進行重新計算,可使用 useMemo 來優化

範例

由於每次畫面更新渲染都會執行 slowFunc,即使我們只想 toggle theme 時,還是要等待 for 迴圈結束,導致 dark theme 的畫面更新會有延遲。

// 模擬複雜計算
const slowFunc = (num) => {
  for (let i = 0; i <= 1000000000; i++) {}
  return num * 2;
};

const Fruit = () => {
  const [num, setNumber] = useState(0);
  const [dark, setDark] = useState(false);
  const doubleNumber = slowFunc(num); // 每次渲染都會執行
  const theme = {
    color: dark ? "white" : "black",
    backgroundColor: dark ? "black" : "white"
  };

  return (
    <>
      <input type="number" value={num} onChange={e => setNumber(parseInt(e.target.value))} />
      <span>{doubleNumber}</span>
      <button onClick={() => setDark(!dark)} style={theme}>
        toggle theme
      </button>
    </>
  );
};

使用 useMemo 改寫並指定依賴後,在 toggle theme 就不會再進入 for 迴圈,能及時變更顏色了

const slowFunc = (num) => {
  for (let i = 0; i <= 1000000000; i++) {}
  return num * 2;
};

const Fruit = () => {
  const [num, setNumber] = useState(0);
  const [dark, setDark] = useState(false);
  // 指定 num 為依賴的值,只有 num 改變值,才呼叫 slowFunc
  const doubleNumber = useMemo(() => slowFunc(num), [num]);
  const theme = {
    color: dark ? "white" : "black",
    backgroundColor: dark ? "black" : "white"
  };

  return (...);
};

useCallback

使用方法

// 語法: const cb = useCallback(callback, [依賴項])

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
  • useMemo 概念大致相同,useMemo 儲存 function return 回來的值useCallback 儲存整個 functionuseCallback(fn, deps) 相等於 useMemo(() => fn, deps)
  • 可傳入依賴 array,當依賴改變時才重新宣告跟建立 function
  • 使用情境:當子組件有依賴父組件傳遞 function,父組件的 function 可使用 useCallback 優化,來防止不必要的 render

範例

當每次 Parent Component(<Foo>)的 numdark 值變動時, 畫面會重新渲染,<Foo> 內的 getItems 也跟著被重新宣告跟建立,等於 Child Component(<List>) 每次接收不同的 getItems,也跟著重新渲染(即使值不變,只有 dark 變動的情境)。

- 不使用 useCallback

// Child Component
const List = ({ getItems }) => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(getItems());
    console.log("executed"); // num 或 dark 值變動時,<List> 都會 rerender,印出 executed
  }, [getItems]);

  return items.map(item => <div key={item}>{item}</div>);
};

// Parent Component
const Foo = () => {
  const [num, setNumber] = useState(1);
  const [dark, setDark] = useState(false);

  // 一般宣告函式
  const getItems = () => [num, num + 1, num + 2];

  const theme = {
    color: dark ? "white" : "black",
    backgroundColor: dark ? "black" : "white"
  };

  return (
    <>
      <input type="number" value={num} onChange={e => setNumber(parseInt(e.target.value))} />

      <button onClick={() => setDark(prev => !prev)} style={theme}>
        toggle theme
      </button>

      <List getItems={getItems} />
    </>
  );
};

- 使用 useCallback

使用 useCallback,並傳入 num 當作依賴項,接下來當 num 值改變時,getItems 這個 function 才會重新被建立。

// Child Component
const List = ({ getItems }: { getItems: () => number[] }) => {
  const [items, setItems] = useState<number[]>([]);

  useEffect(() => {
    setItems(getItems());
    console.log("executed"); // 只有在 num 值變動時,才印出 executed
  }, [getItems]);

  return items.map(item => <div key={item}>{item}</div>);
};

// Parent Component
const Foo = () => {
  const [num, setNumber] = useState(1);
  const [dark, setDark] = useState(false);

  // 使用 useCallback
  const getItems = useCallback(() => [num, num + 1, num + 2], [num]);

  const theme = { ... };

  return (
    <>
      <input type="number" value={num} onChange={e => setNumber(parseInt(e.target.value))} />

      <button onClick={() => setDark(prev => !prev)} style={theme}>
        toggle theme
      </button>

      <List getItems={getItems} />
    </>
  );
};

參考資料

  1. React 官方文件
  2. Web Dev Simplified - React Hooks
ReactReact Hooks
Published on 08 Dec 2021
Updated on 08 Dec 2021