您的某些元件可能需要控制 React 以外的系統並與之同步。例如,您可能需要使用瀏覽器 API 將焦點放在輸入框上、播放和暫停非 React 實作的影片播放器,或者連線到遠端伺服器並監聽訊息。在本章中,您將學習讓您可以「跳出」React 並連接到外部系統的額外機制。您的大部分應用程式邏輯和資料流不應依賴這些功能。
本章內容
使用 refs 參考值
當您希望元件「記住」某些資訊,但您不希望該資訊觸發新的渲染時,您可以使用ref
const ref = useRef(0);
與狀態類似,refs 在重新渲染之間由 React 保留。但是,設定狀態會重新渲染元件。更改 ref 不會!您可以透過 ref.current
屬性存取該 ref 的目前值。
import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return ( <button onClick={handleClick}> Click me! </button> ); }
ref 就像您元件的一個秘密口袋,React 不會追蹤它。例如,您可以使用 refs 來儲存逾時 ID、DOM 元素,以及其他不會影響元件渲染輸出的物件。
使用 refs 操作 DOM
React 會自動更新 DOM 以符合您的渲染輸出,因此您的元件通常不需要操作它。但是,有時您可能需要存取 React 管理的 DOM 元素,例如,將焦點放在節點上、捲動到它,或者測量它的大小和位置。React 中沒有內建的方法可以執行這些操作,因此您需要 DOM 節點的 ref。例如,點擊按鈕將使用 ref 將焦點放在輸入框上
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
使用 Effects 同步
某些元件需要與外部系統同步。例如,您可能希望根據 React 狀態控制非 React 元件、設定伺服器連線,或者在元件出現在螢幕上時傳送分析記錄。與允許您處理特定事件的事件處理常式不同,Effects 允許您在渲染後執行一些程式碼。使用它們將您的元件與 React 以外的系統同步。
按幾次播放/暫停,看看影片播放器如何與 isPlaying
屬性值保持同步
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
許多 Effects 也會自行「清理」。例如,設定與聊天伺服器連線的 Effect 應傳回一個清理函式,該函式告訴 React 如何將您的元件與該伺服器斷開連線
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
在開發過程中,React 會立即額外執行並清理您的 Effect 一次。這就是為什麼您會看到 "✅ 連線中..."
列印兩次的原因。這可確保您不會忘記實作清理函式。
你可能不需要 Effect
Effect 是 React 範式中的一個逃生出口。它們允許你「跳出」React 並將你的元件與某些外部系統同步。如果沒有涉及外部系統(例如,如果你想在某些 props 或 state 改變時更新元件的 state),你應該不需要 Effect。移除不必要的 Effect 將使你的程式碼更容易理解、執行速度更快且更不容易出錯。
有兩種常見的情況下你不需要 Effect
- 你不需要 Effect 來轉換渲染用的資料。
- 你不需要 Effect 來處理使用者事件。
例如,你不需要 Effect 來根據其他 state 調整某些 state
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
相反地,盡可能在渲染時進行計算
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
然而,你*確實*需要 Effect 來與外部系統同步。
反應式 Effect 的生命週期
Effect 的生命週期與元件不同。元件可以掛載、更新或卸載。Effect 只能做兩件事:開始同步某些東西,以及稍後停止同步它。如果你的 Effect 依賴於隨著時間推移而變化的 props 和 state,則此週期可能會發生多次。
此 Effect 依賴於 roomId
prop 的值。Props 是*反應式值*,這表示它們可以在重新渲染時更改。請注意,如果 roomId
改變,Effect 會*重新同步*(並重新連接到伺服器)。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
React 提供了一個程式碼檢查規則來檢查你是否已正確指定 Effect 的依賴項。如果你忘記在上述範例的依賴項列表中指定 roomId
,程式碼檢查器會自動找到該錯誤。
將事件與 Effect 分離
事件處理程式僅在你再次執行相同互動時才會重新執行。與事件處理程式不同,如果 Effect 讀取的任何值(例如 props 或 state)與上次渲染時不同,Effect 就會重新同步。有時,你希望混合使用這兩種行為:一個響應某些值而不是其他值的 Effect。
Effect 內的所有程式碼都是*反應式的*。如果它讀取的一些反應式值由於重新渲染而發生更改,它將再次執行。例如,如果 roomId
或 theme
發生更改,此 Effect 將重新連接到聊天室
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
這並不理想。你只想在 roomId
改變時才重新連接到聊天室。切換 theme
不應該重新連接到聊天室!將讀取 theme
的程式碼從你的 Effect 移到*Effect 事件*中
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Effect 事件內的程式碼不是反應式的,因此更改 theme
不會再使你的 Effect 重新連接。
移除 Effect 依賴項
當你編寫 Effect 時,程式碼檢查器會驗證你是否已將 Effect 讀取的每個反應式值(例如 props 和 state)包含在 Effect 的依賴項列表中。這可確保你的 Effect 與元件的最新 props 和 state 保持同步。不必要的依賴項可能會導致你的 Effect 執行過於頻繁,甚至造成無限迴圈。移除它們的方式取決於具體情況。
例如,此 Effect 依賴於每次編輯輸入時都會重新建立的 options
物件
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const options = { serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
你不希望每次在該聊天室中開始輸入訊息時都重新連接到聊天室。要解決此問題,請將 options
物件的建立移到 Effect 內,以便 Effect 僅依賴於 roomId
字串
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
請注意,你不是從編輯依賴項列表開始移除 options
依賴項。那樣是錯誤的。相反地,你更改了周圍的程式碼,使依賴項變得*不必要*。將依賴項列表視為你的 Effect 程式碼使用的所有反應式值的列表。你不應該刻意選擇要在此列表中放置什麼。列表描述了你的程式碼。要更改依賴項列表,請更改程式碼。
使用自訂 Hooks 重複使用邏輯
React 內建了一些 Hooks,例如 useState
、useContext
和 useEffect
。有時,您會希望有一個 Hook 能夠處理更特定的用途:例如,擷取資料、追蹤使用者是否在線上,或連線到聊天室。為此,您可以根據應用程式的需求建立自己的 Hooks。
在此範例中,自訂 Hook usePointerPosition
會追蹤游標位置,而自訂 Hook useDelayedValue
會傳回一個值,該值會「延遲」您傳遞的值一定的毫秒數。將游標移到沙盒預覽區域,即可看到一串移動的點跟隨著游標。
import { usePointerPosition } from './usePointerPosition.js'; import { useDelayedValue } from './useDelayedValue.js'; export default function Canvas() { const pos1 = usePointerPosition(); const pos2 = useDelayedValue(pos1, 100); const pos3 = useDelayedValue(pos2, 200); const pos4 = useDelayedValue(pos3, 100); const pos5 = useDelayedValue(pos4, 50); return ( <> <Dot position={pos1} opacity={1} /> <Dot position={pos2} opacity={0.8} /> <Dot position={pos3} opacity={0.6} /> <Dot position={pos4} opacity={0.4} /> <Dot position={pos5} opacity={0.2} /> </> ); } function Dot({ position, opacity }) { return ( <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> ); }
您可以建立自訂 Hooks、將它們組合在一起、在它們之間傳遞資料,並在元件之間重複使用它們。隨著應用程式的成長,您將會減少手動編寫 Effects 的次數,因為您將能夠重複使用已編寫的自訂 Hooks。React 社群也維護許多優秀的自訂 Hooks。
下一步是什麼?
前往使用 Refs 參考值,開始逐頁閱讀本章!