當您希望元件「記住」某些資訊,但不希望該資訊觸發新的渲染時,您可以使用一個ref。
您將學習
- 如何將 ref 新增到您的元件
- 如何更新 ref 的值
- Refs 與狀態有何不同
- 如何安全地使用 refs
將 ref 新增到您的元件
您可以透過從 React 匯入 useRef
Hook 將 ref 新增到您的元件
import { useRef } from 'react';
在您的元件內,呼叫 useRef
Hook 並將您想要參考的初始值作為唯一參數傳遞。例如,這裡是一個參考值 0
的 ref
const ref = useRef(0);
useRef
會傳回像這樣的物件
{
current: 0 // The value you passed to useRef
}

插圖作者 Rachel Lee Nabors
您可以透過 ref.current
屬性存取該 ref 的目前值。這個值刻意設計為可變的,這表示您可以讀取和寫入它。它就像您元件中的一個秘密口袋,React 不會追蹤它。(這就是它成為 React 單向資料流「逃生艙口」的原因——詳情請見下文!)
在這裡,一個按鈕會在每次點擊時遞增 ref.current
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 指向一個數字,但就像狀態一樣,您可以指向任何東西:字串、物件,甚至函式。與狀態不同,ref 是一個具有 current
屬性的純 JavaScript 物件,您可以讀取和修改它。
請注意,元件不會隨著每次遞增而重新渲染。 與狀態一樣,refs 在重新渲染之間由 React 保留。但是,設定狀態會重新渲染元件。更改 ref 不會!
範例:建立碼錶
您可以在單個元件中組合 refs 和狀態。例如,讓我們製作一個使用者可以透過按下按鈕來啟動或停止的碼錶。為了顯示自使用者按下「開始」以來經過了多少時間,您需要追蹤開始按鈕何時被按下以及目前的時刻。此資訊用於渲染,因此您將其保存在狀態中:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
當使用者按下「開始」時,您將使用 setInterval
每 10 毫秒更新一次時間
import { useState } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); function handleStart() { // Start counting. setStartTime(Date.now()); setNow(Date.now()); setInterval(() => { // Update the current time every 10ms. setNow(Date.now()); }, 10); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> </> ); }
當按下「停止」按鈕時,您需要取消現有的間隔,使其停止更新 now
狀態變數。您可以透過呼叫 clearInterval
來執行此操作,但您需要提供先前在使用者按下開始時由 setInterval
呼叫返回的間隔 ID。您需要將間隔 ID 保留在某處。由於間隔 ID 未用於渲染,您可以將其保存在 ref 中:
import { useState, useRef } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> <button onClick={handleStop}> Stop </button> </> ); }
當一則資訊用於渲染時,請將其保存在狀態中。當一則資訊僅由事件處理程式需要,並且更改它不需要重新渲染時,使用 ref 可能更有效率。
Refs 與狀態之間的差異
或許您會認為 refs 看起來不如狀態那樣「嚴格」——例如,您可以改變它們,而不必總是使用狀態設定函式。但在大多數情況下,您會想要使用狀態。Refs 是一個您不需要經常使用的「逃生艙口」。以下是狀態和 refs 的比較
refs | 狀態 |
---|---|
useRef(initialValue) 傳回 { current: initialValue } | useState(initialValue) 傳回狀態變數的目前值和一個狀態設定函式 ( [value, setValue] ) |
當您更改它時不會觸發重新渲染。 | 當您更改它時會觸發重新渲染。 |
可變的——您可以在渲染過程之外修改和更新 current 的值。 | 「不可變的」——您必須使用狀態設定函式來修改狀態變數以將重新渲染排入佇列。 |
您不應該在渲染期間讀取(或寫入) current 值。 | 您可以隨時讀取狀態。然而,每次渲染都有其自身的狀態快照,而這個快照是不會改變的。 |
這裡有一個使用狀態實作的計數器按鈕
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> You clicked {count} times </button> ); }
因為count
值會被顯示,所以使用狀態值來表示它是有意義的。當計數器的值被setCount()
設定時,React 會重新渲染元件,螢幕也會更新以反映新的計數。
如果您嘗試使用 ref 來實作這個功能,React 將永遠不會重新渲染元件,因此您永遠不會看到計數變化!請注意,點擊此按鈕並不會更新其文字
import { useRef } from 'react'; export default function Counter() { let countRef = useRef(0); function handleClick() { // This doesn't re-render the component! countRef.current = countRef.current + 1; } return ( <button onClick={handleClick}> You clicked {countRef.current} times </button> ); }
這就是為什麼在渲染期間讀取ref.current
會導致程式碼不可靠的原因。如果您需要這樣做,請改用狀態。
深入探討
雖然useState
和useRef
都是由 React 提供的,但原則上useRef
可以在useState
的基礎上實作。您可以想像在 React 內部,useRef
的實作方式如下
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
在第一次渲染期間,useRef
會回傳{ current: initialValue }
。這個物件會被 React 儲存,因此在下次渲染時,會回傳相同的物件。請注意,在此範例中,狀態設定器並未使用。這是因為useRef
始終需要回傳相同的物件,所以不需要使用它!
React 提供了內建版本的useRef
,因為它在實務中很常見。但您可以將它視為沒有設定器的普通狀態變數。如果您熟悉物件導向程式設計,ref 可能會讓您想起實例欄位—但您撰寫的是somethingRef.current
,而不是this.something
。
何時使用 ref
通常情況下,當您的元件需要「跳出」React 並與外部 API 進行通訊時,您會使用 ref,這通常是指不會影響元件外觀的瀏覽器 API。以下列出一些罕見的情況
如果您的元件需要儲存某些值,但它不影響渲染邏輯,請選擇 ref。
ref 的最佳實務
遵循這些原則將使您的元件更具可預測性
- 將 ref 視為逃生出口。 當您使用外部系統或瀏覽器 API 時,ref 很有用。如果您的應用程式邏輯和資料流程大多依賴 ref,您可能需要重新思考您的方法。
- 不要在渲染期間讀取或寫入
ref.current
。 如果在渲染期間需要某些資訊,請改用狀態。由於 React 不知道ref.current
何時會更改,即使在渲染時讀取它也會讓您的元件行為難以預測。(唯一的例外是像if (!ref.current) ref.current = new Thing()
這樣的程式碼,它只會在第一次渲染期間設定 ref 一次。)
React 狀態的限制不適用於 ref。例如,狀態的作用類似於每次渲染的快照,並且不會同步更新。但當您變更 ref 的目前值時,它會立即更改
ref.current = 5;
console.log(ref.current); // 5
這是因為ref 本身就是一個普通的 JavaScript 物件,所以它的行為就像一個物件一樣。
當您使用 ref 時,您也不必擔心避免變更。只要您正在變更的物件沒有用於渲染,React 就不會關心您如何處理 ref 或其內容。
Ref 與 DOM
您可以將 ref 指向任何值。然而,ref 最常見的用例是存取 DOM 元素。例如,如果您想以程式設計方式將焦點放在輸入上,這會很方便。當您在 JSX 中將 ref 傳遞給ref
屬性時,例如<div ref={myRef}>
,React 會將對應的 DOM 元素放入myRef.current
中。一旦元素從 DOM 中移除,React 會將myRef.current
更新為null
。您可以在使用 Ref 操作 DOM中了解更多相關資訊。
重點回顧
- Refs 是一個用於保存不需渲染的值的逃生艙口(escape hatch)。你不會經常用到它們。
- 一個 ref 是一個普通的 JavaScript 物件,它只有一個名為
current
的屬性,你可以讀取或設定它。 - 你可以透過呼叫
useRef
Hook 來要求 React 提供一個 ref。 - 與狀態一樣,refs 讓你可以在元件重新渲染之間保留資訊。
- 與狀態不同的是,設定 ref 的
current
值不會觸發重新渲染。 - 不要在渲染期間讀取或寫入
ref.current
。這會讓你的元件難以預測。
挑戰 1之 4: 修復損壞的聊天輸入框
輸入訊息並點擊「發送」。你會注意到在看到「已發送!」的提示訊息之前有三秒鐘的延遲。在此延遲期間,你可以看到一個「取消」按鈕。點擊它。這個「取消」按鈕應該要停止顯示「已發送!」訊息。它透過在 handleSend
期間儲存的逾時 ID 呼叫 clearTimeout
來做到這一點。然而,即使點擊了「取消」,「已發送!」訊息仍然會出現。找出它無法運作的原因,並修復它。
import { useState } from 'react'; export default function Chat() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); let timeoutID = null; function handleSend() { setIsSending(true); timeoutID = setTimeout(() => { alert('Sent!'); setIsSending(false); }, 3000); } function handleUndo() { setIsSending(false); clearTimeout(timeoutID); } return ( <> <input disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <button disabled={isSending} onClick={handleSend}> {isSending ? 'Sending...' : 'Send'} </button> {isSending && <button onClick={handleUndo}> Undo </button> } </> ); }