React Hooks 核心機制與效能優化 應用 State 與 Effect 實戰範例提升高品質開發效率

me
林彥成
2024-08-17 | 7 min.
文章目錄
  1. 1. 什麼是 React Hooks?
  2. 2. React Hooks 介紹:為什麼它改變了開發模式?
  3. 3. React 元件的兩種形式
  4. 4. useState 範例:深入理解狀態管理機制
    1. 4.1. useState 純值與物件比較
    2. 4.2. useState(Primitive) vs useState(Object)
    3. 4.3. JavaScript 物件比較
    4. 4.4. Module Pattern 與 useState 的比較
  5. 5. useEffect 教學:處理副作用與生命週期
    1. 5.1. 使用 useEffect 的實際案例
    2. 5.2. useEffect 的常見問題與解決方案
  6. 6. React 效能優化:Hooks 的進階實踐技巧
    1. 6.1. React 18 useState Automatic Batching
  7. 7. React Hooks 優點與缺點全面解析
    1. 7.1. React Hooks 優點
    2. 7.2. 缺點
    3. 7.3. 壓縮 (minified)
  8. 8. FAQ:React Hooks 常見問題快速解答
    1. 8.1. Q1:為什麼不能在 if 條件式中調用 Hooks?
    2. 8.2. Q2:useState 與 useReducer 該如何選擇?
    3. 8.3. Q3:如何避免 useEffect 產生的無限迴圈?

什麼是 React Hooks?

React Hooks 是 React 16.8 版本引入的一項核心功能,它允許開發者在不撰寫類別元件(Class Component)的情況下,於函式元件(Function Component)中使用狀態管理、副作用處理等功能。透過核心的 useStateuseEffect,開發者能更直覺地組織程式碼、提升邏輯重用性並優化效能,是現代 React 開發必備的技能。

React Hooks 介紹:為什麼它改變了開發模式?

React 引用的 Hooks 設計,讓副作用處理和外部功能更輕鬆地進入函式元件,相比於傳統類別元件 (Class Component) 的寫法,提供了更簡潔且強大的解決方案。在這份 React Hooks 教學 中,我們將探討這種模式如何簡化開發流程。

舉例來說,Redux 的 useSelector 可以完全取代繁瑣的 connectmapStateToProps。接下來,我們將通過 useState 範例useEffect 教學,深入對比其與類別寫法的核心差異。

React 元件的兩種形式

React 元件有兩種形式:類別(Class)和函式(Function)。這兩種形式在狀態管理上有所不同:

  • Class 元件:通過 this.setState() 來直接設定元件中的狀態。
  • Function 元件:使用 const [state, setState] = useState() 來回傳的 setState() 函數進行狀態更新。
1
2
3
4
5
6
7
8
9
// 使用 connect 的寫法
const mapStateToProps = (state) => ({
counter: state.myState,
});

export default connect(mapStateToProps)(App);

// 使用 hook 的寫法
const counter = useSelector((state) => state.myState);

useState 範例:深入理解狀態管理機制

useState 是一個讓函式元件擁有狀態的 Hook。它的運作方式類似於 JavaScript 的閉包模式,讓你可以在函式中管理狀態,而不是使用類別元件中的 this.state。這是在學習 React Hooks 教學 時最基礎也最重要的部分。

useState 純值與物件比較

狀態可以是基本型別(Primitive)或物件型別(Object):

  • Primitive type:通過值傳遞(by value)。
  • Object type:通過引用傳遞(by reference)。

在 React 中,使用 useState 時的考量包括:

  • Primitive:比較單純,適合新手使用。
  • Object:對於多層物件結構,建議將物件進行正規化 (normalize)以簡化比較和更新。

useState(Primitive) vs useState(Object)

在使用 Hook 時,可以採用以下兩種策略來處理複雜狀態:

  • useState(Primitive):宣告多個 useState,每個狀態變數單獨管理。
  • useState(Object):使用一個 useState,將多個狀態存儲在一個物件中。

由於 class 的狀態一定是物件的型態,對於 Object 型態的狀態會有比較好的處理,舉例來說像是物件的合併機制。

1
2
// class setState 實際上做的事情
this.setState({ ...state, value: 0 });

相對於 hook 在設定上實際上沒有針對物件做預設的物件合併機制。

1
2
3
//  setHookState 其實就是單純設定
const [state, setHookState] = useState(0);
setHookState();

JavaScript 物件比較

由於物件型態是 by reference ,使用上會有需要考量相等的比較方式。

  • 這個例子來說物件並不相等 {display: "flex"} === {display: "flex"} // false

在 JavaScrpit 中有三種比較方式

  • strict equality operator ===
  • loose equality operator ==
  • Object.is()

在 React 中會用到的方式是 shallow object equality check

  • Shallow Equality: 會迭代物件物件中的 keys 如果都是 === 就當他相同
    • React.PureComponent 的 shouldComponentUpdate 實作的判斷方式

物件型態是按引用比較的,即 {display: "flex"} === {display: "flex"} // false
React 使用淺層比較來判斷物件是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}

const hero1 = {
name: "Batman",
address: {
city: "Gotham",
},
};
const hero2 = {
name: "Batman",
address: {
city: "Gotham",
},
};
// 兩層
shallowEqual(hero1, hero2); // => false

Module Pattern 與 useState 的比較

Module Pattern 使用 IIFE(Immediately Invoked Function Expression)來創建私有變數和公共函式,類似於 useState 提供的狀態管理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const moduleCounter = (function () {
let count = 0;
return {
getValue: function () {
return count;
},
increment: function () {
return ++count;
},
reset: function () {
console.log("reset:" + count);
count = 0;
},
};
})();

這與 useState 類似,都是利用閉包來保存狀態和操作函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Counter() {
const [count, setCount] = useState(0);

function increment() {
setCount(count + 1);
}

function reset() {
setCount(0);
}

function getValue() {
return count;
}

return (
<div>
<button type="button" onClick={increment}>
add
</button>
<button type="button" onClick={reset}>
reset
</button>
<span>{getValue()}</span>
</div>
);
}

useEffect 教學:處理副作用與生命週期

useEffect 用來處理副作用,相當於類別元件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount。它接受兩個參數:副作用函式和依賴陣列。掌握 useEffect 教學 的關鍵在於理解依賴陣列如何影響元件的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// componentDidMount
useEffect(() => {}, []);

// componentDidUpdate
useEffect(() => {
const subscription = props.source.subscribe();
}, [props.source]);

useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// componentWillUnMount
subscription.unsubscribe();
};
}, [props.source]);

使用 useEffect 的實際案例

例如,從 API 取得資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async componentDidMount() {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
};

async componentDidUpdate(prevProps) {
if (prevProps.resource !== this.props.resource) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
}
};

const fetchUsers = async () => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`);

setUsers(response.data);
};

useEffect( () => { fetchUsers(users) }, [ users ] );

由於 React 是 component-based 的一個函式庫,所以元件本身的定義和規範就蠻重要的,其中比較特殊的是元件在實際運用上會有一些生命週期,大致上我們平常會使用到的就是 componentDidMountcomponentDidUpdate,剩下可能會用到但比較少的是 componentWillUnmount

由於是寫在 function 中,所以可以想像整個 function 的內容都是原來寫法中 render() 裡的內容,差別在把 constructor 中的狀態用其他的方法寫在這個 function 裡面,元件原本由狀態改變來驅動的特性一樣沒有改變。

更好的寫法則是再抽出來,就會變下面這樣,更清楚也更好測試,也可以重複的去使用相關邏輯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function useCounter(initial) {
const [count, setCount] = useState(initial);
const increment = () => setCount(count + 1);
const reset = () => setCount(0);
return { reset, count, increment };
}

function Counter() {
const [reset, count, increment] = useCounter(0);

return (
<div>
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={reset}>reset</button>
<span>{count)}</span>
</div>
);
}

useEffect 的常見問題與解決方案

  1. 依賴缺失警告 (useEffect has a missing dependency):確保所有在 useEffect 中使用的變數都包含在依賴陣列中。
  2. 元件卸載後更新 (update on a unmounted component):在清理函式中處理 API 請求或計時器的中止。
1
2
3
4
useEffect(() => {
fetchData();
updateCloumn();
}, []);
  1. useEffect 中去打 API,但是資料回來時使用者已經切換到其他畫面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useEffect(() => {
let controller = new AbortController();
(async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{
signal: controller.signal,
}
);
setList(await response.json());
controller = null;
} catch (e) {
// Handle the error
}
})();
// aborts the request when the component umounts
return () => controller?.abort();
});
  1. timer function 也是同樣的概念
1
2
3
4
5
6
7
useEffect(() => {
let timerId = setTimeout(() => {
timerId = null;
setList([]);
}, 1000);
return () => clearTimeout(timerId);
});
  1. websocket 由於跟 API 的概念不太一樣,做法就是直接關掉
1
2
3
4
5
useEffect(() => {
const webSocket = new WebSocket(url);

return () => webSocket.close();
});

React 效能優化:Hooks 的進階實踐技巧

在撰寫 hook 且狀態較複雜時,為了達成最佳的 React 效能優化,通常會有底下兩種策略:

  • useState(Primitive): 宣告多個 useState 搭配多個值
  • useState(Object): 宣告一個 useState 搭配一個物件中含有多個值

在 React 18 版之前,宣告多個 State 的寫法如下:

1
2
3
4
5
6
7
8
9
10
const [isLoading, setLoading] = useState(true);
const [list, setlist] = useState([]);
useEffect(async () => {
fetch("/list")
.then((res) => res.json())
.then((data) => {
setlist(data);
setLoading(false);
});
}, []);

這樣的寫法會因為兩個 setState 發生在非同步完成之後,所以 React 並不會 batch 他們而造成兩次的畫面渲染。

所以在 React 還沒協助處理之前,最簡單的方式就是透過物件一次設定,或是使用 unstable_batchedUpdates

1
2
3
4
setStateOnce({
list: data,
isLoading: false,
});

有篇 Github 上的討論蠻精彩的,有興趣可以參考如下:
https://github.com/reactwg/react-18/discussions/21

內文中也有提供範例,大家可以進去試用看看兩者差異。

在剛開始寫 hook 的時候,可能會寫像是以下的程式碼。下面 console.log 會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState } from "react";

const CountDisplay = ({ count }) => <h3>Count: {count}</h3>;

const CountButton = ({ updateCount }) => {
// 下面這行會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免
console.log("=> CountButton render", Date.now());

return <button onClick={() => updateCount()}>Increment</button>;
};

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

const updateCount = () => setCount((c) => c + 1);

return (
<>
<CountButton updateCount={updateCount} />
<CountDisplay count={count} />
</>
);
};

export default App;

除了基本的 hook 以外,常用的還有以下:

  • useState: 加入元件狀態,但這裡的 setState 不會幫我們自動合併物件型態的狀態,需要用 callback 方式寫並且自行合併
  • useReducer: 可以用來處理物件型態的狀態
  • useEffect: 處理 side effect,取代 componentDidMount, componentDidUpdate, componentWillUnmount
  • useCallback: 當 function 需要在 useEffect 中被使用但又不想加入觸發條件
  • useMemo: 把較高成本計算記起來
  • useRef: 取得參考用的 object

剛剛解決方案的小提示

  1. useMemo: 可以記住函式運算值
  2. useCallBack: 可以記住函式

React 18 useState Automatic Batching

前面提到的問題將在 React 18 之後獲得解決,即使是非同步 callback 中的 setState 也會自動 batch 而不會造成兩次的渲染,還是想分開反而要自己使用 ReactDOM.flushSync 來分開。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
setCount((c) => c + 1); // Does not re-render yet
setFlag((f) => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}

return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}

function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
fetchSomething().then(() => {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount((c) => c + 1); // Causes a re-render
setFlag((f) => !f); // Causes a re-render
});
}

return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}

React Hooks 優點與缺點全面解析

React Hooks 優點

  • 更接近原生 JavaScript,對於初學者友好,且不需要理解 ES6。
  • 減少複雜的元件週期管理,只需控制 useEffect
  • 提供簡化的解決方案,如 Redux 的 useSelector
  • 更容易進行程式碼壓縮和最佳化。

缺點

  • useEffect 可能會將多個副作用合併,需謹慎管理依賴。
  • 避免在函式中使用 new 或未處理的事件監聽器,避免每次渲染時都重做一次。
  • 尚未涵蓋 getSnapshotBeforeUpdatecomponentDidCatch 這兩個生命周期方法。

壓縮 (minified)

React 團隊指出,Hook 是純函式,這使得程式碼壓縮(minified)變得更簡單,相對於類別元件來說更易於進行壓縮和優化。

FAQ:React Hooks 常見問題快速解答

Q1:為什麼不能在 if 條件式中調用 Hooks?

A:React 依賴 Hooks 調用的順序來正確對應狀態。如果在條件式中使用,渲染順序可能會改變,導致狀態對應錯誤。

Q2:useState 與 useReducer 該如何選擇?

A:當狀態邏輯簡單時,使用 useState;當狀態邏輯複雜(涉及多個子值)或下一個狀態依賴於前一個狀態時,useReducer 能提供更清晰的架構。

Q3:如何避免 useEffect 產生的無限迴圈?

A:檢查依賴陣列。確保陣列中的變數不會在 useEffect 內部被修改,或者對於物件與函式型別的依賴,使用 useMemouseCallback 穩定其引用。

注意: 詳細閱讀 React Hooks 文件 以了解更多信息。


喜歡這篇文章,請幫忙拍拍手喔 🤣