EP04 | React 18 全家桶複習 - Hooks的使用

Function Component 不存在生命週期,這也是為什麼 React 開始推薦使用函數組件的原因,可以減少學習成本。

useEffect()

作用: 為函數組件處理副作用。

副作用定義: 副作用是相對主作用來說的,一個函數除了主作用,其他的都是副作用。對 React 而言,主作用就是根據數據(state/props)渲染頁面,除此都是副作用。

常見副作用:

  1. 手動修改 DOM
  2. localStorage 操作
  3. 數據請求 AJAX 發送

React 官方文檔:
每次 render 後都會執行 useEffect 嗎? 是的!預設情況下,它在第一個 render 和隨後每一個更新之後執行。你可能會發現把 effect 想成發生在「render 之後」更為容易,而不是考慮「mount」和「更新」。 React 保證 DOM 在執行 effect 時已被更新
每次重新 render 時,我們都會安排一個 different effect 來替代上一個。在某種程度上,這使 effect 的行為更像是 render 結果的一部分 — 每個 effect 都「屬於」特定的 render (useEffect 會在應用下一個 effect 之前,清除之前的 effect)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 先導入 useEffect
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useSate(0);
const [name, setName] = useSate("Celeste")

// count 的值要10s後才會被console(非同步)
useEffect(()=>{
setTimeout(()=>{
console.log(count)
}, 10000)
}) // 會在第一個 render 和隨後每一次更新渲染(任何數據有變更)之後執行

return (
<div>
{count}
{name}
<button onClick={() => {setName("Smith")}}>改name</button>
<button onClick={() => {setCount(++count)}}>改 count</button>
</div>
)
}

效能優化 ⬇️

在目前的 useEffect 中,在 name 有變化時,useEffect 並不需要去被執行,因為它所依賴的是 count,為了讓 useEffect 可以僅在 count 有變化才執行,就需要使用 useEffect 第 2 個參數(為一個陣列),該陣列需列出所有依賴項。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useSate(0);
const [name, setName] = useSate("Celeste")

useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 10000)
}, [count]) // 只會在第一個 render 和隨後每一次 count 更新渲染之後執行

return (
<div>
{count}
{name}
<button onClick={() => {setName("Smith")}}>改name</button>
<button onClick={() => {setCount(++count)}}>改 count</button>
</div>
)
}

避免內存洩漏 ⬇️
有些程式碼如果不執行清除的話,在組件被卸載時,仍會占用內存(如: setInterval),所以需要再 useEffect 中返回一個 function ,來讓 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
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useSate(0);
const [name, setName] = useSate("Celeste")

useEffect(()=>{
const timer = setInterval(()=>{
console.log(count)
}, 10000)
// 用於清除 setInterval
return () => {
clearInterval(timer);
}
}, [count]) // 只會在第一個 render 和隨後每一次 count 更新渲染之後執行

return (
<div>
{count}
{name}
<button onClick={() => {setName("Smith")}}>改name</button>
<button onClick={() => {setCount(++count)}}>改 count</button>
</div>
)
}

React 官方文檔提出: useEffect 會在應用下一個 effect 之前,清除之前的 effect。這也代表 React 會在執行新的 useEffect 之前,先執行上一個的 useEffect 的清除(也就是 return 的函數)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化
const timer = setInterval(() => {
console.log(count);
}, 10000); // 執行第一個 Effect

// 第一次數據更新
clearInterval(timer); // 清除上一個的 Effect
const timer = setInterval(() => {
console.log(count);
}, 10000); // 執行新的 Effect

// 第二次數據更新
clearInterval(timer); // 清除上一個的 Effect
const timer = setInterval(() => {
console.log(count);
}, 10000); // 執行新的 Effect

useRef()

作用: 獲取真實 DOM 或組件的方法。
原則: 不能對 function component 使用 ref。

什麼時候該使用 Ref:

  1. 管理 focus、選擇文字、或影音播放。
  2. 觸發即時的動畫。
  3. 與第三方 DOM 函式庫整合。
    React 提示不要過度使用 Ref,在可以使用 state 管理數據的位置,就應該使用 state。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useEffect, useState, useRef } from "react";
export default function App() {
const [name, setName] = useSate("Celeste")

// 宣告 nameEl 使 ref 可以參考到它
const nameEl = useRef(null); // 返回值為一個對象,內部有一個 current 對象存放指定的 DOM
const ageEl = useRef(null);

useEffect(()=>{
console.log(ageEl);
// 對 ageEl 所對應的 DOM (透過 ageEl.current 取得) 執行指定動作
ageEl.current.focus();
})

return (
<div>
{/* 透過 ref 來指定與哪個 useRef 連結*/}
姓名: <input ref= {nameEl} />
年齡: <input ref= {ageEl} />
<button onClick={() => {setName("Smith")}}>改name</button>
</div>
)
}

useCallback()

作用: 防止因為組件重新渲染,導致方法被重新創建,起到緩存作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
impport { useState, useCallback } from "react";
export default function App(){
const [count, setCount] = useState(0);
const [name, setName] = usesState("Celeste");

// useCallback(fn, [依賴的數組])
// fn 內的資料都會是緩存的資料,所以在依賴(count)沒有改變的情況下,緩存的資料(name)都不會改變
// changeCount 本質是一個函數
const changeCount = useCallback(()=>{
console.log(name); // Celeste
setCount(++count)
}, [count]);

return (
<div>
count: {count}
<button onClick={changeCount}>改變 count</button>
<hr/>
{name}
{/* 當 name 被改變,App 組件被更新載入時,並不會再次去創建 changeCount */}
<button onClick={()=> {setName("Smith")}}>改變 name</button>
</div>
)
}

useMemo()

作用: 防止因為組件重新渲染,導致方法所返回的值被重新計算創建,起到緩存作用,與useCallback 不同的是, useMemo 會執行第一個參數(函數)並返回一個值,並且儲存該返回值,之後只要依賴沒改變,就不會重新執行。

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
impport { useState, useMemo } from "react";
export default function App(){
const [count, setCount] = useState([1, 2, 3]);
const [name, setName] = usesState("Celeste");

// useMemo(() => 返回值, [依賴的數組])
// useMemo 緩存的是計算結果,只要 count 不改變,則 addCount 就不會被重新執行計算結果值
// addCount 本質是一個值
const addCount = useMemo(() => () => {
console.log(name); // Celeste
let sum = 0;
count.forEach(num => { sum+= num })
return sum
}, [count]);

return (
<div>
count: {count.map((num)=><span>{num}</span>)},
總計: {addCount()}
<button onClick={changeCount}>改變 count</button>
<hr/>
{name}
{/* 當 name 被改變,App 組件被更新載入時,並不會重新去計算 addCount */}
<button onClick={()=> {setName("Smith")}}>改變 name</button>
</div>
)
}

對於 useMemo 使用時機的更多了解: Memorized Hook 在幹嘛?