React 重新複習 hooks

剛看完官方文件,來留個紀錄。

簡述

之前有針對各個 hooks 寫過系列筆記:

但當時還沒有仔細看過官方文件,可能寫得不是很完整,所以這篇會寫得更詳細一些。

其他補充

舉凡像是 setStatedispatch 等等這些從 hook 回傳的 function,都不會在 re-render 的時候被改變,所以才可以不用在 useEffectuseCallback 把它們傳到 dependencies 中。

useState

用來產生「state」的 function

回傳值有兩個,第一個是 state 的值,第二個是改變 state 時會用到的 setter:

1
const [counter, setCounter] = useState(0);

所以要在一個元件顯示 state 的話只要:

1
<button>{counter}</button>

就會看到一個 0 的按鈕了。

而要改變 state 的話就要透過 setter 來改變,像這樣:

1
2
// 用 setCounter 來更新 state
<button onClick={() => setCounter(prev => prev + 1)}>{counter}</button>

這樣子每點一下按鈕 counter 的值就會加一。

另外我想補充一個觀念,就是每次 re-render 時 state 會怎麼運作?

像我之前就有想過,如果 re-render 等於重新執行一次 function component 的內容的話,那裡面的 useState 不就又會重新執行一次了嗎?這樣的話 state 不就會又變成最開始的初始值了?(0)

列個流程,應該會比較好理解我的意思:

  1. 第一次 render
  2. 執行 useState(0),counter 的初始值為 0
  3. 點按鈕,更新 state 讓 counter 變成 1,觸發 re-render
  4. 再執行一次 useState(0),counter 的值為 0?還是 1?

我們當然知道答案會是 1,但這是為什麼?原來官方文件裡面有提到:

During subsequent re-renders, the first value returned by useState will always be the most recent state after applying updates.

簡單來說 re-render 時 useState 確實會被重新執行,但它會確保第一個回傳值(也就是 state)是最新的那個值。

最後補充幾個其他特性:

  • 更新 state 的方式有兩種,直接傳值的叫做「normal form」,透過 prev 的叫做「functional form」,如果新的 state 必須用 prev 值來計算的話建議用後者比較安全
  • 如果新的 state 跟 目前的 state 一樣,就不會觸發 re-render
  • 在 hook 裡面就算只有要改 state 的某個部分,也必須把完整的 state 給放進去,而不是像 class component 一樣可以只寫要改的地方就好(這個很重要哦)
  • 如果 state 的初始值得用複雜計算來求出,可以在 useState 裡改傳進一個 function,這個只會在第一次 render 的時候被執行,後續的不會,技術上稱為「Lazy initial state」
  • 假設一次更新兩個 state,React 會自動合在一起,變成只 re-render 一次。在 18.0 以前只有 Event handler 裡面的更新 state 會有這效果,但這之後就不侷限了,想看範例可以參考這裡

useEffect

就跟課程講的一樣,意思是:

render 完以後想做什麼?

為什麼會取名為「Effect」?其實是因為 React 希望你用這個 hook 來處理「side effect(副作用)」,意思是指跟 React 本身無關的事情,像是「發 API 拿資料」、「儲存到 storage」等等。

這些 effect 雖然很重要(因為有可能會改變內容),但對 React 在渲染 Component 的時後來說是不重要的,所以才會說這是 side effect。

總之呢,不要把一些跟 Component 沒有直接關係的東西寫在裡面,像這樣:

1
2
3
function Component() {
fetch('...');
}

而是要這樣:

1
2
3
4
5
6
7
8
function Component() {
return (...)
}

// render 完以後才去發 API
useEffect(() => {
fetch('...')
})

不過 useEffect 預設是每次 render 以後都會被執行,所以更準確的作法是加上第二個參數(dependencies)來告訴它「當某某值改變的時候再幫我執行這個 effect」:

1
2
3
4
5
6
7
8
9
function Component() {
return (...)
}
// 不管傳什麼第一次都一定會被執行
// 所以這邊傳空的代表我只希望他被執行一次
useEffect(() => {
// render 完以後才去發 API
fetch('...')
}, [])

基本上呢,只要是出現在 useEffect 裡的 props 或 state,都應該要放在 dependencies 裡面,否則當值改變時它們還是會停留在舊的那個值,這個要多注意一下。

接著做一個補充,useEffect 每次在執行 side effect 以前會先把上一次的 effect 給清掉,這個階段叫做「clean up effect」,這樣做是為了確保「memory weak(簡單來說就是記憶體浪費吧?)」的問題。

另外 useEffect 裡面其實還可以在回傳另一個 function,它會在 component 從畫面上移除前給執行,像這樣:

1
2
3
4
5
6
7
8
function Message() {
useEffect(() => {
// render 以後執行
console.log("Message has rendered.");
// 回傳 function,被移除前會執行
return () => console.log("before Message remove.");
});
}

想看實際效果可以到 這邊 看。

useContext

把 props 聚集到一個地方管理

簡單來說就是這樣。

舉個例子,當你的 props 要通過「很多層來傳遞時」,可能就會寫出這樣的東西:

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
39
40
41
42
43
// 要往下傳遞的 props
const theme = {
light: {
bg: "white",
color: "black",
},
dark: {
bg: "black",
color: "white",
},
};

// 第三層,終於拿到 props
function Content({ theme }) {
return (
<div
style={{
backgroundColor: theme.bg,
color: theme.color,
}}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio eos neque
explicabo soluta excepturi impedit aperiam necessitatibus fugiat officia
recusandae quidem consequuntur, minus facere suscipit quam asperiores
ipsum. Impedit, facilis?
</div>
);
}

function Wrapper({ theme }) {
// 第二層
return <Content theme={theme} />;
}

function App() {
const [isDarkmode, setIsDarkMode] = useState(true);
return (
<div className="App">
// 第一層
<Wrapper theme={theme.dark} />
</div>
);
}

有同學用說這樣的場景就跟「接力棒」很像,我覺得這還蠻貼切的 XD

但如果改用 useContextcreateContext 就能大大節省掉這個困擾:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const theme = {
light: {
bg: "white",
color: "black"
},
dark: {
bg: "black",
color: "white"
}
};

// 1. 建立 Context
const ThemeContext = createContext(null);

function Content() {
// 3. 取得 Context 的值
const theme = useContext(ThemeContext);

return (
<div
style={{
backgroundColor: theme.bg,
color: theme.color
}}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio eos neque
explicabo soluta excepturi impedit aperiam necessitatibus fugiat officia
recusandae quidem consequuntur, minus facere suscipit quam asperiores
ipsum. Impedit, facilis?
</div>
);
}

function Wrapper() {
return <Content />;
}

function App() {
const [isDarkmode, setIsDarkMode] = useState(true);
return (
// 2. 建立 Provider 和要提供的值(value)
<ThemeContext.Provider value={isDarkmode ? theme.dark : theme.light}>
<div className="App">
<Wrapper />
// 按下按鈕時切換主題
<button onClick={() => setIsDarkMode((prev) => !prev)}>
switch theme
</button>
</div>
</ThemeContext.Provider>
);
}

ReactDOM.render(<App />, document.getElementById("root"));

想看效果的話可以到 這裡 看。

總之呢,像這樣把 props 集中到 Context 來處理會好管理很多,也不用像剛剛那麼 hardcode 每一層都得傳一遍。

useReducer

去學 redux 吧,畢竟這跟它有關。

簡單來說就是用來取代 useState 的另一種 hook,會需要它是因為當 state 很多或很複雜的時候,要傳遞各種 props 也會變得很麻煩,所以才會用這種類似「flux」方式來集中管理。

這邊先大概知道怎麼用就好,其他的等學 Redux 再說吧!

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
// state 初始值
const initialState = { count: 0 };

// reducer,用來管理 state 的工具人
// 會根據 action 來更新 state
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
};

function App() {
// 使用它,要記得傳入前面寫的 reducer 跟 state
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div className="App">
count: {state.count}
// 接著透過 dispatch 跟 reducer 說我想做什麼
<button onClick={() => dispatch({type: "increment"})}>+</button>
<button onClick={() => dispatch({type: "decrement"})}>-</button>
</div>
);
}

效果可以到 這邊 看。

總之,這樣的好處是不用在透過 props 把更新 state 的 callback 傳給子元件,子元件只要先 useReducer,再透過 dispatch 來指派任務就完事了。

useCallback

把 function 記起來。

這個是拿來做優化時比較會用到。舉例來說,我們可能會在一個 Component 中定義 function:

1
2
3
4
5
function App() {
const [someState, setSomeState] = useState("hello");
const doSomething = () => console.log(someState);
...
}

但這樣的問題在於,不管怎麼樣每次 re-render 的時候 doSomething 都會在重新宣告一次,即使 someState 沒有變也一樣,這樣不是很浪費嗎?明明沒有必要?

所以 useCallback 就是用來解決這個問題的,它可以把一個 function 給記住,你只要告訴它「xxx 改變的時候再幫我重新宣告就好了」。

這邊附上一個用來驗證的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function App() {
const [value, setValue] = useState("");

// 一個有用 useCallback,一個沒有
const logHello = () => console.log("Hello");
const logHelloMemorized = useCallback(logHello, []);

// 把一開始的 reference 記起來
// (利用 useRef 在 re-render 後不會變的特性)
const refLogHello = useRef(logHello);
const refLogHelloMemorized = useRef(logHelloMemorized);

return (
<div className="App">
<div>Is same logHello: {refLogHello.current === logHello ? "Yes" : "No"}</div>
<div>Is same logHelloMemorized: {refLogHelloMemorized.current === logHelloMemorized ? "Yes" : "No"}</div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
</div>
);
}

這邊刻意放了一個 <input>,目的是要讓我在輸入文字時觸發 re-render。

所以在 re-render 時,logHello 理應就會被重新宣告一次,變成一個新的 function,而 logHelloMemorized 不會,因為它有用 useCallback 包住。

建議你到我寫的 範例 去試試看,再來思考我這邊說的意思應該就能理解了。

至於 dependencies 該傳什麼?就跟 useEffect 的邏輯差不多,只要是 function 裡會用到的 state 或 props 幾乎都要放進去,不然就很容易出現 bug。

為什麼?因為 function 裡的值是舊的

useMemo

把回傳值給記住

useCallback 一樣是用來優化的 hook。

簡單來說,可以把複雜的計算放在 useMemo 裡面來做,然後一樣給它 dependencies,告訴它什麼東西改變時在重新計算就好,這樣就能省下不必要的計算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function App() {
const [value, setValue] = useState("");

// 每次 re-render 都會被執行
const veryComplexCalculate = () => {
console.log('calculate')
return 1 + 1
}

// 只有 dependencies 改變時才執行
const veryComplexCalculateByMemo = useMemo(() => {
console.log('memo')
return 100 + 100
}, [])


return (
<div className="App">
<div>Result1: {veryComplexCalculate()}</div>
<div>Result2: {veryComplexCalculateByMemo}</div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
</div>
);
}

一樣可以到我寫 範例 參考(記得打開 console)

useRef

一個 Mutable 的值,只有你自己去改它時它才會變,而且不會觸發 re-render

附註:不會變的原理是因為 React 在背後幫你做了一些事情,讓每次 re-render 都會是同一個 Object 的 reference。

上面的說法沒有很完整,但我覺得這樣講會好記一點,一樣來看個範例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function App() {
const [randomValue, serRandomValue] = useState(0);
// {current: 0}
const counter = useRef(0)
// 用來觸發 re-render
const changeValue = () => serRandomValue(Math.random())

return (
<div className="App">
<div>Ref: {counter.current}</div>
// 因為是 Mutable 所以能直接改
<button onClick={() => counter.current++}>add 1 to ref</button>
<button onClick={changeValue}>Re-render</button>
</div>
);
}

首先 useRef 會建立一個物件,它會有一個 current 屬性,用來儲存你給的初始值。所以你要存取東西都要用 .current 來取,不然會拿到整個物件。

它最大的特點就是「Mutable」,所以你可以直接改,不用像改 state 一樣還要先 copy 一份新的才行。

這邊一樣有寫 範例 來練習,可以參考看看。

除了上面這個範例,它還有一個很常會拿來用的地方:「儲存 DOM 元素」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
const [flag, setFlag] = useState(true);
// {current: null}
const inputRef = useRef(null)

useEffect(() => {
// 每次 render 完如果有抓到 input 就 focus
if (inputRef.current) {
inputRef.current.focus();
}
});

return (
<div className="App">
// 把 input 節點存到 useRef 裡
{ flag && <input ref={inputRef} />}
// 點按鈕可以把 input 隱藏 or 顯示
<button onClick={() => setFlag(prev => !prev)}>Toggle</button>
</div>
);
}

這邊的範例可以到 這邊 試。

總之這背後的原理是,只要把一個物件放在 ref 屬性上,React 就會自動幫你把 DOM 元素放到 current 裡,但一般還是建議用 useRef 來存比較好。

useImperativeHandle

useRef 的延伸

這個應該不太會用到,只要大概知道一下就好了。

這個是用在如果我把一個 Component 拆到最小單位,像是只有一個 <input>,那我有沒有辦法在在父元件去控制它?聽起來有點莫名奇妙吧?所以來直接看範例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 我想操控的那個 input
import Myinput from "./Myinput";

function App() {

// 一樣要透過 useRef 來儲存 DOM
const inputRef = useRef(null);
const focusInput = () => {
// 這邊待會在解釋,你只要知道這邊的意思是用來 focus 就好了
inputRef.current.yoyoyo();
}

return (
<div className="App">
// 把這個 Component 存到 useRef
<Myinput ref={inputRef} />
<button onClick={focusInput}>focus</button>
</div>
);
}

export default App;

簡單來說,<Myinput /> 在建立的時候可以決定要 export 哪些「method」來給父元件用,這邊是 yoyoyo。名字是我故意亂取的所以看起來很詭異,但我只是想讓你知道它是可以自定義的,不用一定要 focus

接著來看一下子元件的部分吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useRef, useImperativeHandle, forwardRef } from "react"

function MyInput (props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
// export 一個 yoyoyo,它可以 focus 到這個 input
return {
yoyoyo: () => inputRef.current.focus()
}
})

return (
<input
type="text"
placeholder="type some thing..."
ref={inputRef}
/>
)
}

export default forwardRef(MyInput)

簡單來說,這邊你要做三件事情:

  1. 把 DOM 元素用 useRef 儲存
  2. 設定 useImperativeHandle,把要 export 出去的東西寫好
  3. 最後用 forwardRef 包起來

就這樣囉,想看範例可以到 這邊

useLayoutEffect

這個也跟課程講的一樣:

render 完以後,瀏覽器 paint 完後想做什麼

它跟 useEffect 基本上是一樣的東西,只差在「觸發的時機」不同,一個是在畫面 paint 完以後執行,一個在 paint 之前。

這邊的示範有點難做,不確定是不是 18.0 有做一些優化,不能做出「閃一下」的效果,有興趣的話可以參考 這篇,裡面有示範什麼是「閃一下」。

或你也可以參考我寫的另一種 範例

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 { useState, useEffect, useLayoutEffect } from "react";

function App() {
const [todos, setTodos] = useState(["todo1", "todo2", "todo3"]);

// 如果是 useLayoutEffect 畫面就會等 3 秒後才 paint 出來
// 如果是 useEffect 畫面會直接顯示,不會受到影響
useLayoutEffect(() => {
const target = new Date().getTime() + 3000;
// 等三秒
while (new Date().getTime() < target) {}
console.log("finish");
}, []);

return (
<div className="App">
<ul>
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
</div>
);
}

總之你只要知道這兩個觸發的「時機點」是不一樣的就好了。另外一般會建議能用 useEffect 就用它,少用 useLayoutEffect

mentor-program-day127 mentor-program-day126
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×