跳至主要内容

[React] useRef

什麼是 useRef

React 文件開宗明義地說了:

When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.

useState 那篇,已經知道 state 的更動會觸發 Render 的機制來更新 DOM 及畫面。
React 這段話的意思是,有些情況我們只是想記住某些資訊,但不希望因此重新渲染元件,這時就可以使用 useRef
這也造就 useRef 天生與 useState 不同的特性,即使他們都是 React 用來儲存狀態的機制。

Differences between refs and state

這是一張 React 官方文件上的比較表,清楚揭示了 useRefuseState 的差異。

refsstate
useRef(initialValue) 回傳 { current: initialValue }useState(initialValue) 回傳目前狀態值與狀態更新函式([value, setValue]
修改時不會觸發重新渲染。修改時會觸發重新渲染
可變(Mutable)— 你可以在渲染過程之外修改 current 的值。「不可變」(Immutable)— 你必須使用狀態設定函式來修改狀態,並排入重新渲染流程。
不建議在渲染期間讀取或寫入 current 的值。你可以在任何時候讀取 state。不過,每次渲染都有自己的 state 快照,該快照不會變動。

When to use refs

對於何時該使用 useRef,React 官方的說法是「當組件需要跳出 React 與外部 api 溝通時」,而這種溝通通常不會影響畫面變化,那就該使用 useRef
如果覺得抽象的話,那可能是對什麼是外部 api 有疑惑,通常性來說,外部 api 指的是 browser api 如 setTimeout 等。
如下述官方範例,因為 interval ID 並不會拿來渲染畫面,它只是會被拿來停止計時器,所以這裡就可以使用 useRef 來儲存 interval ID。

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>
<h1>{intervalRef.current}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
注意

真要說的話,把上面那段改成 useState 也不是什麼大問題,如果只是想要 code 會動的話,而且從實際畫面表現上也看不出差異。

export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const [intervalId, setIntervalId] = useState(null)

function handleStart() {
setStartTime(Date.now());
setNow(Date.now());

clearInterval(intervalId);
const id = setInterval(() => {
setNow(Date.now());
}, 10);
setIntervalId(id);
}

function handleStop() {
clearInterval(intervalId);
}

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>
</>
);
}

但這樣其實違背 React 想把不影響畫面的狀態交給 useRef 的設計初衷,而且可能帶來幾個潛在問題:

  1. 使用 useState 會導致每次更新 interval ID 時都觸發重新渲染,會造成效能浪費。
  2. 如果極快速連續點擊 Start → Stop → Start,使用 useState 可能會因為狀態更新的非同步特性,導致拿到的 intervalId 是舊的(尚未更新為最新 ID),而清錯計時器。相對地,useRef.current 的值是同步更新的變數,可以即時讀取與修改,在這類快速交互的邏輯裡更安全可靠。

Manipulating the DOM with Refs

useRef 另一常見的用途是直接與 DOM 元素互動。

export default function Form() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}

這裡在做的事情其實很簡單,就是在按下按鈕時,讓 input 元素獲得焦點。
這是因為 inputRef.current 已經透過 ref={inputRef} 存入了該 input 元素的 DOM 節點,因此就可以在其他地方使用 inputRef.current 來操作這個 DOM 節點。

Accessing another component’s DOM nodes

官方還有示範如何使用 useRef 來存取另一個組件的 DOM 節點。這通常用於需要跨組件互動的情況。

function MyInput({ ref }) {
return <input ref={ref} />;
}

export default function MyForm() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}

這裡的 MyInput 組件接受一個 ref prop,並將其傳遞給內部的 input 元素。
這樣,MyForm 就可以使用 inputRef 來存取 MyInput 中的 input 元素,並在按下按鈕時讓它獲得焦點。
具體來說,其實是把 child component 的 DOM 交由 parent component 的 useRef 來存取,因此 parent component 得以操縱 child component 的 DOM。