渲染 (rendering) 是 React 應用程序的心臟,當某些東西(屬性,狀態)發生變化時,組件樹會重新渲染,將最新數據反映給使用者界面。到目前為止,useState 一直是描述組件渲染方式的主要工作。但是我們可以做的更多。還有更多的 Hooks 可以定義有關為什麼以及何時進行渲染的規則。還有更多的掛鉤可以增強渲染效能。
之前,我們介紹了 useState,useContext,我們看到可以將這些 Hooks 組合到我們自己的自定義 Hooks 中:useInput 和 useColors。 不過,還有更多,React 附帶了更多可用的 Hooks。再來,我們將仔細研究 useEffect,useLayoutEffect 和 useReducer。所有這些對於構建應用程序都至關重要。我們還將介紹 useCallback 和 useMemo,它們可以幫助優化我們的組件以提高性能。
使用 useEffect
現在,我們對渲染組件時發生的情況有了很好的了解。組件僅僅是渲染用戶界面的函式。渲染在應用程序首次加載以及屬性和狀態(props and state)值更改時發生。但是,有時候我們希望在 React 渲染更新 DOM 之後執行一些額外的程式碼。網路請求、手動變更 DOM、和 logging,該如何做?
我們使用一個簡單的 Checkbox 複選框組件來觀察組件的渲染週期。我們使用 useState 設置一個 checked 值,並使用 setChecked 函式來更改 checked 的值。用戶可以選擇和取消選擇該複選框,但是我們如何提醒用戶該複選框已被選擇呢?
我們使用 console.log 來觀察一下組件的渲染週期:
Checkbox1 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
| <div id="react-container"></div>
<script type="text/babel"> const { useState } = React; const { render } = ReactDOM; const Checkbox = () => { const [checked, setChecked] = useState(false); console.log(`before render: checked: ${checked.toString()}`); return ( <> <input type="checkbox" value={checked} onChange={() => setChecked(checked => !checked)} /> <span className="margin-left-sm">{checked ? "checked" : "not checked"}</span> {console.log(`rendering: checked: ${checked.toString()}`)} </> );
console.log(`after render: checked: ${checked.toString()}`); }; const App = () => ( <> <h1>Hello Tainan!</h1> <Checkbox /> </> ); render ( <App />, document.getElementById("react-container") )
</script>
|
組件的渲染主要是第 12 行到第 22 行 return 的 JSX 程式碼。我們在第 10 行渲染之前、第 20 行渲染時與第 24 行渲染後添加了 console.log。執行的結果是:
1 2
| before render: checked: false rendering: checked: false
|
第 24 行渲染後調用的 console.log 沒有被執行,因為它位於 return 之後,將永遠無法到達該代碼。
為確保渲染後按預期方式看到 console.log,我們可以使用 useEffect。放置在 useEffect 函式內的 console.log 意味著該函式將在渲染後被當成一種副作用(side effect)而被調用。
Checkbox1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const { useState, useEffect } = React; ... const Checkbox = () => { const [checked, setChecked] = useState(false); console.log(`before render: checked: ${checked.toString()}`); useEffect(() => { console.log(`after render: checked: ${checked.toString()}`); }); return ( <> <input type="checkbox" value={checked} onChange={() => setChecked(checked => !checked)} /> <span className="margin-left-sm">{checked ? "checked" : "not checked"}</span> {console.log(`rendering: checked: ${checked.toString()}`)} </> ); };
|
第 1 行加入 useEffect;第 9 行將原來在渲染後調用的 console.log 放置在 useEffect 函式裡,而 useEffect 函式將在渲染後作為副作用而被調用。
1 2 3
| before render: checked: false rendering: checked: false after render: checked: false
|
當渲染需要引起副作用時,我們使用 useEffect。 可以將副作用視為函式除了使用 return 所返回的結果之外還可以完成並影響函式外的事情。 Checkbox 函式使用 return 渲染 UI,但是我們希望 Checkbox 組件做更多的事情。組件執行除 return UI 之外的其他操作稱為 effect。
console.log 或與瀏覽器或本機 API 的互動都不是渲染的一部分,這不是 return 的一部分。在 React 應用程序中,渲染會影響這些事件的結果,我們可以使用 useEffect 等待渲染之後,再將值提供給 console.log:
useEffect1 2 3
| useEffect(() => { console.log(checked ? "Yes, checked" : "No, not checked"); });
|
同樣,我們可以使用渲染時的 checked 值,然後將其存儲到 localStorage 中。
useEffect1 2 3
| useEffect(() => { localStorage.setItem("checkbox-value", checked); });
|
將 useEffect 視為渲染後發生的函式。當渲染觸發時,我們可以訪問組件中當前的狀態值,並使用它們執行其他操作,這些操作都必須在渲染後調用,否則就與渲染的 UI 不相等了。然後,一旦我們再次渲染,整個過程就會重新開始。 新值,新渲染,新 effects。
依賴陣列 (The Dependency Array)
useEffect 的設計旨在與其他有狀態的 Hooks,例如 useState 和迄今未提及的 useReducer 結合使用。當狀態改變時,React 將重新渲染組件樹。然後,在這些渲染之後調用 useEffect。
我們來看看與 useState 結合使用的範例。
useState and useEffect1 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
| const { useState, useEffect } = React; const { render } = ReactDOM; const App = () => { const [val, set] = useState(""); const [phrase, setPhrase] = useState("example phrase"); const createPhrase = (e) => { e.preventDefault(); setPhrase(val); set(""); }; useEffect(() => { console.log(`typing "${val}"`); }); useEffect(() => { console.log(`saved phrase: "${phrase}"`); }); return ( <> <label className="margin-right-sm">Favorite phrase:</label> <input value={val} placeholder={phrase} onChange={e => set(e.target.value)} /> <button className="t-Button t-Button--hot margin-left-sm" onClick={createPhrase} >Send</button> </> ); }; render ( <App />, document.getElementById("react-container") );
|
val 是一個狀態變數,表示輸入字段的值。每次輸入字段的值時 val 都會更改,這會使組件在用戶每次鍵入新字符時渲染 App 組件。當用戶單擊 “Send” 按鈕時,文本區域的 val 會被保存到 phrase 狀態變數,並且重置 val 為空字串,這將清空文本字段。這是一個不斷渲染的過程。
這可以按預期工作,但是 useEffect 掛鉤被調用的次數超過了應有的次數。 每次鍵入新字符渲染後,兩個 useEffect Hooks 均會被調用:
console.log1 2 3 4 5 6 7 8 9 10 11 12
| typing "" // First Render saved phrase: "example phrase" // First Render typing "H" // Second Render saved phrase: "example phrase" // Second Render typing "He" // Third Render saved phrase: "example phrase" // Third Render typing "Hel" // Fourth Render saved phrase: "example phrase" // Fourth Render typing "Hell" // Fifth Render saved phrase: "example phrase" // Fifth Render typing "Hello" // Sixth Render saved phrase: "example phrase" // Sixth Render
|
我們不希望在每個渲染調用所有的 effect。 我們需要將 useEffect 掛鉤與特定的數據更改相關聯,當某些數據更改時才調用 useEffect。為了解決這個問題,我們可以合併依賴陣列(Dependency array)。依賴陣列可用於控制何時調用 useEffect:
Dependency array1 2 3 4 5 6 7
| useEffect(() => { console.log(`typing "${val}"`); }, [val]);
useEffect(() => { console.log(`saved phrase: "${phrase}"`); }, [phrase]);
|
第 3 與第 7 行我們將依賴陣列添加到兩個 effect 中,以控制它們何時被調用。現在,僅在依賴陣列中的值更改時才會調用這些 effect。
1 2 3 4 5 6 7 8 9
| typing "" // First Render saved phrase: "example phrase" // First Render typing "H" // Second Render typing "He" // Third Render typing "Hel" // Fourth Render typing "Hell" // Fifth Render typing "Hello" // Sixth Render typing "" // Seventh Render saved phrase: "Hello" // Seventh Render
|
依賴陣列畢竟是一個陣列,因此可以檢查依賴陣列中的多個值。假設我們想在 val 或 phreae 發生變化時調用 useEffect。
Dependency array1 2 3
| useEffect(() => { console.log("either val or phrase has changed"); }, [val, phrase]);
|
還可以提供一個空陣列,空的依賴陣列會導致 effect 在初始渲染之後僅被調用一次。
Dependency array1 2 3
| useEffect(() => { console.log("only once after initial render"); }, []);
|
依賴陣列中沒有依賴項意味著沒有更改,因此 effect 將不再被調用。 僅在第一個渲染上調用的 effects 對於初始化非常有用:
Dependency array1 2 3
| useEffect(() => { welcome(); }, []);
|
如果從 effect 中返回一個函式,則從組件樹中刪除該組件時將會調用該函式:
Dependency array1 2 3 4
| useEffect(() => { welcome(); return () => goodbye(); }, []);
|
這意味著,您可以使用 useEffect 進行初始設定和卸載。空陣列意味著 welcome 將在第一次渲染時只被調用一次。然後,當組件從組件樹中刪除時,我們將返回一個 goodbye 函式作為清理函式。
此模式在許多情況下很有用。我們可以在第一次渲染時訂閱 WebSocket 服務。 然後,我們將使用清理函式退訂 WebSocket 服務。
useEffect1 2 3 4 5 6 7 8 9 10 11
| const [messages, setMessages] = useState([]); const addMessage = message => setMessages(allMessages => [message, ...allMessages]);
useEffect(() => { wsService.subscribe(addMessage); welcome(); return () => { wsService.unsubscribe(); goodbye(); }; }, []);
|
在一個 useEffect 進行太多工作,看起來很雜亂,也較容易出錯。將功能分為多個 useEffect 調用,通常是一個比較好的主意。
useEffect1 2 3 4 5 6 7 8 9
| useEffect(() => { wsService.subscribe(addMessage); return () => wsService.unsubscribe(); }, []);
useEffect(() => { welcome(); return () => goodbye(); }, []);
|
我們還可以進一步增強它。這是自定義掛鉤 (custom hook) 的好地方。
useWsMessages custom hook1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const useWsMessages = () => { const [messages, setMessages] = useState([]); const addMessage = message => setMessages(allMessages => [message, ...allMessages]); useEffect(() => { wsService.subscribe(addMessage); return () => wsService.unsubscribe(); }, []); useEffect(() => { welcome(); return () => goodbye(); }, []); return messages; };
|
我們的自定義掛鉤包含處理數據的所有功能,這意味著,我們可以輕鬆地與其他的組件共享此功能。在一個名為 MessageList 的新組件中,我們將可以使用自定義掛鉤:
MessageList1 2 3 4 5 6 7 8 9 10 11 12
| const MessageList = () => { const messages = useWsMessages(); return ( <> <h1>{messages.length}</h1> {messages.map(message => ( <Message key={message.id} {...message} /> ))} </> ); };
|
我們可以輕鬆的使用 useWsMessages 自定義掛鉤,當有訂閱的數據到達時,MessageList 組件將會重新渲染。