useRef
useRef 函式會回傳一個可變的 ref 物件,其 .current 屬性被初始化為傳入的參數。並且這個回傳的 ref 物件在整個元件的生命週期中保持不變。
語法
tsx
import { useRef } from 'react';
const ref = useRef(initialValue);
ref.current; // 透過 `.current` 屬性來存取 ref 中保存的值這個 hook 主要用來解決兩個問題:
- 需要取得 DOM 元素或是子元件的參考。
- 保存一個可以跨渲染週期存在的可變值。
透過 ref 操作 DOM 元素
當我們需要直接操作 DOM 元素時,可以使用 useRef 來取得該元素的參考。
常見的需求有:
- 自動 focus 到輸入框。
- 取得元素的屬性(例如寬高、位置等)。
- 控制媒體播放(例如影片、音樂等)。
- 整合第三方函式庫。
範例:自動 focus 到輸入框
tsx
import { useEffect, useRef } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
// 透過 current 屬性來存取 DOM 元素,並呼叫 focus 方法
inputRef.current.focus();
}
}, []);
// 將 ref 屬性綁定到 input 元素上
return <input ref={inputRef} />;
}儲存跨渲染週期的可變值
一個最簡單的例子是用一個 state 來當作計數器 count,每次按下按鈕就會讓 count 加 1。但是如果我們想要在每次渲染時都印出前一次的 count 值,這時候就可以使用 useRef 來保存前一次的值。
tsx
import { useRef, useState } from 'react';
function Count() {
const [count, setCount] = useState(0);
// 單純用一個變數是無法保存前一次的值的,因為每次元件重新渲染時,變數都會被重新初始化
// const prevCount = 0;
const prevCount = useRef(0);
const add = () => {
setCount((c) => c + 1);
prevCount.current = count;
};
return (
<>
<h1>
目前的 count 值:{count},前次 count 值:{prevCount.current}
</h1>
<button onClick={add}>+1</button>
</>
);
}實際寫範例時會出現
Error: Cannot access refs during render的 ESLint 錯誤。這是因為官方文件在注意事項有提到 Do not write or read ref.current during rendering, except for initialization. This makes your component’s behavior unpredictable. 這裡僅為說明useRef的特性,實際使用時請避免在渲染期間讀取或寫入ref.current。
範例:管理計時器
我們來看一個計時器的情境,當使用者按下開始按鈕後,計時器會開始計數,每秒更新一次畫面上的秒數。當使用者按下停止按鈕後,計時器會停止。
我們先來看這段程式碼:
tsx
import { useState } from 'react';
function StopWatch() {
console.log('render');
let timer: number | null = null;
const [count, setCount] = useState(0);
const start = () => {
if (timer) return;
timer = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
console.log(timer);
if (timer) {
clearInterval(timer);
timer = null;
}
};
const reset = () => {
stop();
setCount(0);
};
return (
<div>
<h1>StopWatch: {count}s</h1>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}可以發現到,當我們按下開始按鈕後,計時器正常讀秒,但是當我們按下停止按鈕後,計時器並沒有停止,這是因為 timer 變數在每次元件重新渲染時都會被重新初始化為 null,導致我們無法正確地清除計時器。
我們可以使用 useRef 來解決這個問題,因為 useRef 的值不會因為元件重新渲染而改變:
tsx
import { useRef, useState } from 'react';
function StopWatch() {
console.log('render');
const timer = useRef<number | null>(null);
const [count, setCount] = useState(0);
const start = () => {
if (timer.current) return;
timer.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
console.log(timer);
if (timer.current) {
clearInterval(timer.current);
timer.current = null;
}
};
const reset = () => {
stop();
setCount(0);
};
return (
<div>
<h1>StopWatch: {count}s</h1>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}注意事項
- 元件在重新渲染時,
useRef並不會被重新初始化。 - 修改
ref.current不會觸發元件重新渲染。因為 React 不會知道它何時發生變化,因為 ref 只是一個普通的 JavaScript 物件。 ref.current不能被當作其他 hook 的依賴值使用,因為它的變化不會觸發重新渲染。因此不能把它放在useEffect、useMemo、useCallback等 hook 的依賴陣列中。useRef不能直接用來獲取子元件的實例,需要使用forwardRef(*React 18)。- React 19 之後,可以直接夠過
props將 ref 傳遞給子元件,而不需要使用forwardRef。