React 的第四個 hook:useLayoutEffect

生命週期的味道。

簡述

React render 完,瀏覽器 paint 之前,你想做什麼?

一樣是 todo list 的範例,假設我在讀 loaclStorage 資料的時候是這樣寫的:

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
function App() {

// 預設的初始值
const [todos, setTodos] = useState([
{
id: 0,
content: "吃飯",
isDone: true,
},
{
id: 1,
content: "寫文章",
isDone: false,
},
{
id: 2,
content: "寫程式",
isDone: false,
},
]);

// storage 的資料
useEffect(() => {
const todoData = window.localStorage.getItem("todos") || "";
if (todoData) {
setTodos(JSON.parse(todoData));
}
}, []);
}

這時候就會出現「閃一下」的問題,像這樣:

problem

(我電腦寫不出這種效果,可能是閃太快的關係吧,所以改貼 Lidemy 的影片內容)

這是因為執行順序是這樣:

  1. 第一次 render,瀏覽器 paint 初始值的 todos
  2. 執行 useEffect,讀取 loaclStorage,更新 todos 的 state
  3. state 變了所以再 render 一次,瀏覽器再 paint 成新的 todos

這就是為什麼會「閃一下」,因為 useEffect 是在瀏覽器 paint 完以後才會被執行的 hook,所以一開始會先顯示預設的 todos,接著執行 useEffect 時改了 state,而 state 改變後又重新 render 了一次畫面,最後顯示新的 todos

可以參考這張圖,大概理解一下 hook 的生命週期

hook-flow

總之一定要知道 useEffect 是讚 paint 完以後才會被執行就對了。

回到一開始的問題,那要怎麼解決才好?

從上面的圖你應該能注意到一個東西是「Run LayoutEffects」,看起來就是在說它是跑在「Browser paints screen」以前的 hook,那麼是不是用這個就可以解決了?

沒錯,正是如此哦!

1
2
3
4
5
6
useLayoutEffect(() => {
const todoData = window.localStorage.getItem("todos") || "";
if (todoData) {
setTodos(JSON.parse(todoData));
}
}, []);

改成這樣以後的執行順序就會變成:

  1. 第一次 render,進入瀏覽器 paint 以前的 hook
  2. 執行 useLayoutEffect,讀取 localStorage,更新 todos 的 state
  3. state 變了所以再 render 一次,瀏覽器再把 todos 給 paint 出來

因為是在瀏覽器把畫面 paint 出來以前就先更新了 state,所以原本的 state 在被 render 出來之前就被覆寫掉了,因此最後 paint 時才會是新的 state。(不懂的話就看著圖,想想看每一步的流程應該就會懂了)

PM2 基本指令 React 的第三個 hook:useEffect
Your browser is out-of-date!

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

×