前言
這篇是 useEffect,在這個 Hook 寫完之後就會直接進入實戰了,話不多說,請直接開始閱讀文章!
文章架構
如上篇,我會按照官方文件的架構,首先講解定義,再用一些簡單的範例講解應該怎麼使用這些 Hooks,最後再講解一些容易犯錯的用例。
定義
這個 Hook 的用途是當你需要與 React 外部的東西互動時,將這些外部互動包成一組 “Effect”,用來將切開元件與外部系統的互動,並且避免不穩定的多次渲染。
useEffect(setup, dependencies?)
我們可以看到他的初始化分成兩個部分:
- 傳入
- setup: 傳入一個 function,與外部系統互動的細節,包含當系統結束時的後處理,為一組完整的 Effect。
- dependencies: 可忽略,當需要時傳入一組 Array,做為這組 Effect 的依賴,當依賴的值改變時,就會觸發後處理,並且重新初始化,互動 / 連結外部系統。
範例
以下是使用 useEffect 的範例。
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
// Effect setup function
const connection = createConnection(serverUrl, roomId);
connection.connect();
// 後處理 function
return () => {
connection.disconnect();
};
// dependencies
}, [serverUrl, roomId]);
// ...
}
官方給了幾個 use case,像是連接到聊天室 server、監聽全域事件(window: pointermove)、觸發動畫、控制對話框以及控制觀察者模式等等,以下以連接到聊天室 server 與監聽全域事件。
// use case 1.
// connnect to chatroom server.
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
// 將 serverUrl 設為 state,就可以作為 dependencies 被 useEffect 監聽
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
// setup function: create connection
const connection = createConnection(serverUrl, roomId);
connection.connect();
// clean up function: connection disconnect.
return () => {
connection.disconnect();
};
// dependencies: when serverUrl or roomId changed,
// do clean up first then setup again
}, [serverUrl, roomId]);
// ...
}
// use case 2.
// Listening to a global browser event.
import { useState, useEffect } from 'react';
export default function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// setup function: combine a move cursor handler to listen to window event.
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
// clean up function: remove window event listener.
return () => {
window.removeEventListener('pointermove', handleMove);
};
// no dependencies: so setup and clean up only happend on component render.
}, []);
// ...
}
注意事項
1. 依賴的使用方式 其一
根據傳入的依賴陣列,useEffect 會有不同的行為,這裡總共有三種,分別為有傳入依賴陣列、空依賴陣列和不傳入參數。
// situation 1: with dependencies
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
// situation 2: empty dependencies array
useEffect(() => {
// ...
}, []); // Does not run again (except once in development)
// situation 3: no dependencies
useEffect(() => {
// ...
}); // Always runs again
2. 依賴的使用方式 其二
不要使用 Effect 中會使用的 State 作為依賴傳入 useEffect 中,Effect 更新 State,State 更新 Effect 不斷循環,也就造成不斷執行 useEffect 中的程式碼。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}
3. 依賴的使用方式 其三
別傳入不需要的參數或是物件作為依賴,才不會意外觸發 useEffect。
像是在下方的這個範例中,就不需要將 serverUrl 和 roomId 包起來,serverUrl 不會變動,只需要傳入 roomId 作為依賴觸發 useEffect 就好了。
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
4. 依賴的使用方式 其四
不要使用函式作為依賴項傳入,因為每次都會產生不一樣的值,會一直觸發 useEffect,所以請不要使用函式作為依賴項傳入。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
5. 當元件掛載時 useEffect 都會執行兩次
React 會在使用 Strict Mode 開發的時候,多執行一次 useEffect 用於檢查你的 useEffect 有沒有正確定義,以凸顯特定錯誤。
6. 每次重新渲染 useEffect 都會執行 / 無窮迴圈
請參考依賴的使用方式之一及之二,正確設定依賴就可以避免這些問題。
結語
如果是碰過 Vue 的朋友們就可以感覺到,React 的官方說明文件提到的這些有的沒的錯誤,或是注意事項,在寫 Vue 的同時也有遇到過,只要再繼續深挖這些錯誤,就可以知道我們要做的事將 JS 的特性學好,就可以很直覺地避開這些詭異的錯誤了。
寫完了這些 Hooks 之後,再來我就會直接寫一個專案將這些 Hooks 運用在專案中了,期待 React 為我帶來與 Vue 不同的開發體驗!