[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 官方文件上的比較表,清楚揭示了 useRef
和 useState
的差異。
refs | state |
---|---|
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
的設計初衷,而且可能帶來幾個潛在問題:
- 使用
useState
會導致每次更新 interval ID 時都觸發重新渲染,會造成效能浪費。 - 如果極快速連續點擊 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。