剛看完官方文件,來留個紀錄。
簡述
之前有針對各個 hooks 寫過系列筆記:
- React 的第一個 hook:useState
- React 的第二個 hook:useRef
- React 的第三個 hook:useEffect
- React 的第四個 hook:useLayoutEffect
- React 的第五個 hook:memo
- React 的第六個 hook:useCallback
- React 的第七個 hook:useMemo
- React 的第八個 hook:useContext 與 createContext
但當時還沒有仔細看過官方文件,可能寫得不是很完整,所以這篇會寫得更詳細一些。
其他補充
舉凡像是 setState
或 dispatch
等等這些從 hook 回傳的 function,都不會在 re-render 的時候被改變,所以才可以不用在 useEffect
或 useCallback
把它們傳到 dependencies 中。
useState
用來產生「state」的 function
回傳值有兩個,第一個是 state 的值,第二個是改變 state 時會用到的 setter:
1 | const [counter, setCounter] = useState(0); |
所以要在一個元件顯示 state 的話只要:
1 | <button>{counter}</button> |
就會看到一個 0 的按鈕了。
而要改變 state 的話就要透過 setter 來改變,像這樣:
1 | // 用 setCounter 來更新 state |
這樣子每點一下按鈕 counter 的值就會加一。
另外我想補充一個觀念,就是每次 re-render 時 state 會怎麼運作?
像我之前就有想過,如果 re-render 等於重新執行一次 function component 的內容的話,那裡面的 useState
不就又會重新執行一次了嗎?這樣的話 state 不就會又變成最開始的初始值了?(0)
列個流程,應該會比較好理解我的意思:
- 第一次 render
- 執行
useState(0)
,counter 的初始值為 0 - 點按鈕,更新 state 讓 counter 變成 1,觸發 re-render
- 再執行一次
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 | function Component() { |
而是要這樣:
1 | function Component() { |
不過 useEffect
預設是每次 render 以後都會被執行,所以更準確的作法是加上第二個參數(dependencies)來告訴它「當某某值改變的時候再幫我執行這個 effect」:
1 | function Component() { |
基本上呢,只要是出現在 useEffect
裡的 props 或 state,都應該要放在 dependencies 裡面,否則當值改變時它們還是會停留在舊的那個值,這個要多注意一下。
接著做一個補充,useEffect
每次在執行 side effect 以前會先把上一次的 effect 給清掉,這個階段叫做「clean up effect」,這樣做是為了確保「memory weak(簡單來說就是記憶體浪費吧?)」的問題。
另外 useEffect
裡面其實還可以在回傳另一個 function,它會在 component 從畫面上移除前給執行,像這樣:
1 | function Message() { |
想看實際效果可以到 這邊 看。
useContext
把 props 聚集到一個地方管理
簡單來說就是這樣。
舉個例子,當你的 props 要通過「很多層來傳遞時」,可能就會寫出這樣的東西:
1 | // 要往下傳遞的 props |
有同學用說這樣的場景就跟「接力棒」很像,我覺得這還蠻貼切的 XD
但如果改用 useContext
和 createContext
就能大大節省掉這個困擾:
1 | const theme = { |
想看效果的話可以到 這裡 看。
總之呢,像這樣把 props 集中到 Context 來處理會好管理很多,也不用像剛剛那麼 hardcode 每一層都得傳一遍。
useReducer
去學 redux 吧,畢竟這跟它有關。
簡單來說就是用來取代 useState
的另一種 hook,會需要它是因為當 state 很多或很複雜的時候,要傳遞各種 props 也會變得很麻煩,所以才會用這種類似「flux」方式來集中管理。
這邊先大概知道怎麼用就好,其他的等學 Redux 再說吧!
1 | // state 初始值 |
效果可以到 這邊 看。
總之,這樣的好處是不用在透過 props 把更新 state 的 callback 傳給子元件,子元件只要先 useReducer
,再透過 dispatch
來指派任務就完事了。
useCallback
把 function 記起來。
這個是拿來做優化時比較會用到。舉例來說,我們可能會在一個 Component 中定義 function:
1 | function App() { |
但這樣的問題在於,不管怎麼樣每次 re-render 的時候 doSomething
都會在重新宣告一次,即使 someState
沒有變也一樣,這樣不是很浪費嗎?明明沒有必要?
所以 useCallback
就是用來解決這個問題的,它可以把一個 function 給記住,你只要告訴它「xxx 改變的時候再幫我重新宣告就好了」。
這邊附上一個用來驗證的範例:
1 | function App() { |
這邊刻意放了一個 <input>
,目的是要讓我在輸入文字時觸發 re-render。
所以在 re-render 時,logHello
理應就會被重新宣告一次,變成一個新的 function,而 logHelloMemorized
不會,因為它有用 useCallback
包住。
建議你到我寫的 範例 去試試看,再來思考我這邊說的意思應該就能理解了。
至於 dependencies 該傳什麼?就跟 useEffect
的邏輯差不多,只要是 function 裡會用到的 state 或 props 幾乎都要放進去,不然就很容易出現 bug。
為什麼?因為 function 裡的值是舊的。
useMemo
把回傳值給記住
跟 useCallback
一樣是用來優化的 hook。
簡單來說,可以把複雜的計算放在 useMemo
裡面來做,然後一樣給它 dependencies,告訴它什麼東西改變時在重新計算就好,這樣就能省下不必要的計算:
1 | function App() { |
一樣可以到我寫 範例 參考(記得打開 console)
useRef
一個 Mutable 的值,只有你自己去改它時它才會變,而且不會觸發 re-render
附註:不會變的原理是因為 React 在背後幫你做了一些事情,讓每次 re-render 都會是同一個 Object 的 reference。
上面的說法沒有很完整,但我覺得這樣講會好記一點,一樣來看個範例吧:
1 | function App() { |
首先 useRef
會建立一個物件,它會有一個 current
屬性,用來儲存你給的初始值。所以你要存取東西都要用 .current
來取,不然會拿到整個物件。
它最大的特點就是「Mutable」,所以你可以直接改,不用像改 state 一樣還要先 copy 一份新的才行。
這邊一樣有寫 範例 來練習,可以參考看看。
除了上面這個範例,它還有一個很常會拿來用的地方:「儲存 DOM 元素」
1 | function App() { |
這邊的範例可以到 這邊 試。
總之這背後的原理是,只要把一個物件放在 ref
屬性上,React 就會自動幫你把 DOM 元素放到 current
裡,但一般還是建議用 useRef
來存比較好。
useImperativeHandle
useRef 的延伸
這個應該不太會用到,只要大概知道一下就好了。
這個是用在如果我把一個 Component 拆到最小單位,像是只有一個 <input>
,那我有沒有辦法在在父元件去控制它?聽起來有點莫名奇妙吧?所以來直接看範例吧:
1 | // 我想操控的那個 input |
簡單來說,<Myinput />
在建立的時候可以決定要 export 哪些「method」來給父元件用,這邊是 yoyoyo
。名字是我故意亂取的所以看起來很詭異,但我只是想讓你知道它是可以自定義的,不用一定要 focus
。
接著來看一下子元件的部分吧:
1 | import { useRef, useImperativeHandle, forwardRef } from "react" |
簡單來說,這邊你要做三件事情:
- 把 DOM 元素用
useRef
儲存 - 設定
useImperativeHandle
,把要 export 出去的東西寫好 - 最後用
forwardRef
包起來
就這樣囉,想看範例可以到 這邊
useLayoutEffect
這個也跟課程講的一樣:
render 完以後,瀏覽器 paint 完後想做什麼
它跟 useEffect 基本上是一樣的東西,只差在「觸發的時機」不同,一個是在畫面 paint 完以後執行,一個在 paint 之前。
這邊的示範有點難做,不確定是不是 18.0 有做一些優化,不能做出「閃一下」的效果,有興趣的話可以參考 這篇,裡面有示範什麼是「閃一下」。
或你也可以參考我寫的另一種 範例:
1 | import { useState, useEffect, useLayoutEffect } from "react"; |
總之你只要知道這兩個觸發的「時機點」是不一樣的就好了。另外一般會建議能用 useEffect
就用它,少用 useLayoutEffect
。