⚓ useState

  • useState
  • updater

什麼是 useState

在元件裡面透過 useState 讓每個元件保有自己獨立的 State(狀態)。

何時使用 useState

  1. 該 state 只需要在該元件中使用
  2. 狀態改變時,元件需要重新繪製

使用 updater 取得最新的 state

setState 是非同步執行的,若要馬上取得最新的 state,需使用 updater。

其實 updater 就只是 setState 的 Callback 函式而已,預設有兩個引數,分別是最新的 state 值與最新的 props 值。

處理物件或陣列的注意事項

處理物件或陣列時,要先產生一個全新的物件或陣列再更新 State,因為它們是參考到記憶體位置。

陣列可以用展開運算子或 Array.prototype.map;物件可以用展開運算子或 Object.assign 處理。

⚓ useEffect

  • useEffect
  • 生命週期
  • componentDidMount / componentDidUpdate / componentWillUnmount

生命週期

  1. 元件渲染完 = componentDidMount(就是 Vue 的 mounted)
  2. 元件 state 改變前/後 = componentDidUpdate(Vue 的 watch 有這些功能)
  3. 元件被移除時 = componentWillUnmount(感覺類似 Vue 的 beforeDestroyed)

依賴(第二個參數)

useEffect 第二個參數可決定是否要觸發函式,接收一個陣列,也被稱作該作用的依賴。

  • 如果完全「沒寫」依賴,那這個函式會在每一次渲染時都觸發
  • 如果依賴只放一個「空陣列」,那就代表函式只在渲染後觸發第一次
  • 如果「有寫」依賴,就只有當依賴中的 state 改變,並造成畫面渲染時,才會觸發函式

依賴的寫法會影響如何執行

1. 在每次元件渲染時執行

如果 Dependency 直接「省略」不寫(沒有 Dependency)的話,表示沒有要對比的東西。這樣會導致每個元件渲染時,都會執行函式。

2. 只在元件渲染後執行一次

Dependency 放「空陣列」的話,就只會執行一次函式。

使用情境:獲取資料、添加事件監聽。

3. 在依賴的狀態改變時執行

當 Dependency 陣列裡面的狀態改變,導致畫面重新渲染後,就會觸發 useEffect 的函式。

4. 在依賴的狀態改變、元件被移除之前執行

與上一個情形類似,也是在 state 改變的時候會執行,不過這裡會以「改變前的 state 值」來觸發執行。

要捕捉到 state 改變前的值,我們要在 useEffect 的函式中回傳 (return) 一個函式,這個函式會比上面那個原本的函式還要先被觸發。

使用情境:停止獲取資料、清除監聽。

可以看以下的範例幫助理解:

useEffect(() => {
  1;
  return () => {
    2; // 使用 state 改變前的值
  };
});
  • 元件被渲染完成時,執行 (1)
  • 元件被移除前,執行 (2)
  • 元件內的 state 改變,導致元件重新渲染的話,先執行 (2) 再執行 (1)

⚓ React.memo 與 useMemo

  • React.memo
  • useMemo

使用情境

當元件的 state 改變時,除了該元件重新渲染之外,其實整個 DOM 全部都會被重新渲染!

因為有些不需要改變的地方也被重新渲染了,所以這時候就會被稱為「不必要的渲染」。

改善方法 (1) React.memo

使用 React.memo 把不要被渲染的元素包起來,就能夠避免不必要的重新繪製了。

原理

React.memo 的原理是以 props 有無改變,來判斷是否需要重新渲染。

因此如果根本沒有 props,就代表不會有變化的時候,也就表示不會被重新渲染了!

const Title = React.memo(() => {
    return {
        <div>
            <h1>不會被重新渲染</h1>
        </div>
    }
});

傳入參數

React.memo 的第一個參數是回傳一個元件。

第二個參數則是可以傳入一個函式,它有兩個引數,分別是舊的 props 與新的 props。最後回傳時如果回傳 false 就會渲染(表示新舊 props 不同),回傳 true 則不會渲染。

改善方法 (2) useMemo

使用情境與解決的問題都和 React.memo 一樣,但這是 Hooks 版本的寫法。

const Title = useMemo(()=> {
    return {
        <div>
            <h1>不會被重新渲染{title}</h1>
        </div>
    }
}, [title]);

參數

第一個參數是函式。

第二個參數是依賴的 state,這與 useEffect 相同:

  • 只有當依賴被改變時,才會重新執行函式並回傳新的結果
  • 沒寫第二個參數 = 任何 state 改變都會重新執行函式
  • 只寫空陣列 = 只會執行第一次,永遠不會重新執行

⚓ useCallback

  • useCallback + updater
  • 「效能優化」是在效能出現問題時才處理,不用對還沒出現問題的元件超前部署一堆多餘的配置

目的 & 使用情境

跟 useMemo 目的一樣,都是要避免不必要的渲染,不過 useCallback 可以處理一些特殊情況。

例如:元件重新渲染時,因為 React.memo 與 useMemo 都是做「淺層比較」,所以就算出現內容完全一樣的函式,但由於是不同的物件實體,所以它們會認為兩者是不同的,便會讓元件重新渲染。

大概原理

useMemo 就像是記憶住函式執行的「結果」,而 useCallback 則是記憶住「整個函式」的宣告。

搭配 setState 的 updater

使用 useCallback 時,如果函式裡面有 setState 可能會出現更新不了的問題?!

這是因為 React 的 state 更新是 immutable,所以每次更新都需要一個新的記憶體位置,然而 useCallback 卻是用一個記憶體位置記住函式的宣告,所以會無法更新。

const increment = useCallback(() => {
  setCount(count + 1); // count 永遠都是最初始的 0
});

上方程式碼中,就算怎麼 setState,在 useCallback 裡面的 state 都還是最初的那個記憶體位置,而不是更新後的記憶體位置。

解決方法就是使用之前提到的 updater,來強迫 setState 使用最新的值!

const increment = useCallback(() => {
  setCount((newCount) => newCount + 1); // 使用 count 最新的值
});

⚓ Custom Hooks

我們也可以創造自己的 Hooks,把可以共用邏輯的函式抽出去,製作成 useXXX 的自定義 Hooks,讓不同元件可以共享相同的一套邏輯。

舉例來說,一般的計數器點一次會加一,但今天有個超級計數器點一次要加 10,此時就可以把計數的邏輯拆分出去。

// 新建一個自定義 Hooks 名為 useCounter.js

import { useState, useEffect } from 'react';

const useCounter = (initialCount, callbackFunction) => {
  // 使用外部提供的初始值
  const [count, setCount] = useState(initialCount);

  // 外部使用 add 函式時可以決定一次加多少
  const add = (added) => {
    setCount(count + added);
  };

  // 加上 count 變更時的作用
  useEffect(callbackFunction, [count]);

  // 用物件格式 {count, add} 也可以
  return [count, add];
};

export default useCounter;

我們給 useCounter 以下功能:

  • 參數 1 (initialCount):從外部接收 count 的初始值
  • 參數 2 (callbackFunction):從外部接收一個函式,當 count 改變時會執行
  • 提供外部 count 的值
  • 提供外部一個函式 (add),用來處理 count 的計算邏輯,一次加多少 (added) 由外部使用時決定

最後回傳的格式用陣列或物件都可以,解構時記得使用對應的格式即可。

// 使用 useCounter
import useCounter from '../../hooks/useCounter';

const SuperCounter = () => {
  // 把初始值與函式傳給 useCounter,並解構取出回傳的 count 與 add
  const [count, add] = useCounter(100, () => console.log('超級計數器執行了'));
  // 使用 useCounter
  return (
    <>
      <div>目前的超級數字:{count}</div>
      <button
        onClick={() => {
          add(10); // 點一次加 10
        }}
      >
        點我可以一次加 10
      </button>
    </>
  );
};

以上資源是我自己整理過後的筆記,若有錯誤歡迎隨時和我聯繫