前端框架
-useRef 教學 - React 存取 DOM 與保持資料一致的 Hook
this.web

什麼是 useRef
useRef
是 React 提供的一個 Hook,它可以用來保存一個值,而這個值會被存在一個物件裡面,這個物件再更新組件時,會保持一致的資料,不會被清空。
因為會被保存在物件中,而物件在 JS 裡是參考值,所以這個 Hook 才叫做 useRef
(Reference 參考值)。
useRef 的語法
useRef 的語法很簡單:
import { useRef } from 'react';
function MyComponent() {
const myRef = useRef(initialValue);
// ... 其他程式碼
}
要注意的是,useRef(initialValue)
會返回一個只有單一屬性 current
的物件。也就是說如果你要使用或更改裡面的值,你必須這樣做:
myRef.current = 'otherValue';
console.log(myRef.current); // otherValue
這個 current
初始時會被設定為你傳入的 initialValue
(可以是任意類型的值),而且這個初始值只在第一次 render 時有效。
後續的重新渲染中,useRef
都會回傳同一個物件,也就是說 ref.current
在組件生命週期內會持續保存前一次更新後的值,而不會每次重置。
useRef 和 useState 的差別
是否觸發組件重新渲染
useRef
和 useState
都是用來儲存值的,他們不同的地方在於:useRef
的值改變不會觸發組件的重新渲染。
當我們修改 useRef
的值時,React 不會因為我們修改就更新組件,這代表 useRef
非常適合儲存那些**不影響畫面的資料,例如計時器的 ID、歷史紀錄等等。**例如下面這個計時器的範例:
import React, { useState, useRef, useEffect } from 'react';
function SimpleTimer() {
const [seconds, setSeconds] = useState(0);
// 使用 useRef 儲存計時器 ID,修改時不會觸發重新渲染
const intervalRef = useRef(null);
// 用來儲存 interver 的 id,並在卸載時清除
useEffect(() => {
intervalRef.current = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
return <p>已運行: {seconds} 秒</p>
}
同步與非同步
除此之外,useRef
的更新是會馬上生效的:
const countRef = useRef(0);
function handleClick() {
countRef.current += 1;
console.log(countRef.current); // 1 - 立刻得到最新值
}
而當我們使用 useState
,其實只是告訴 React 說要排程一次更新,接著 React 會先搜集要更新的 state,並統一更新。這代表 useState
值的更新是會稍微延遲的:
const [count, setCount] = useState(0);
function handleClick() {
setCount(prev => prev + 1);
console.log(count); // 0 這裡拿到的是舊值,因為還沒重新渲染
}
延伸閱讀:batch update 是什麼?React 非同步的狀態更新。
useRef 的實際運用
由於 useRef 可以在多次渲染中抱持值的一致,所以很適合用來儲存 DOM 元素,也是最常見的應用場景。如果我們想獲取某個特定元素以及他的資料,就可以這樣用:
import { useRef, useEffect } from 'react';
function Comp() {
const pRef = useRef(null);
useEffect(() => {
console.log(pRef.current?.textContent); // Hello World
}, []);
return (
<p ref={pRef}>
{/* 透過 ref={pRef} 獲取特定 DOM 元素 */}
Hello World
</p>
);
}
再舉一個例子,如果我們想控制一個影片的播放暫停,我們也可以使用 useRef,並將 videoRef 傳給 video,接著就能控制影片
import { useRef } from 'react';
function VideoPlayer() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return (
<>
<video ref={videoRef} width="320" src="video.mp4" />
<button onClick={play}>播放</button>
<button onClick={pause}>暫停</button>
</>
);
}
除了用來抓取 DOM,將 useRef
用來儲存資料也非常實用,因為他不會讓 react re-render 組件,所以在某些場景可以優化效能,例如我想要儲存滑鼠的位置但滑鼠位置不影響畫面的更新,就可以使用 useRef
而不是 useState
。
import { useRef, useEffect } from 'react';
export default function MousePositionRef() {
const mousePosRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const handleMove = e => {
mousePosRef.current = { x: e.clientX, y: e.clientY };
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
const showPosition = () => {
const { x, y } = mousePosRef.current;
alert(`目前滑鼠位置:(${x}, ${y})`);
};
return <button onClick={showPosition}>顯示滑鼠位置</button>;
}
forwardRef 和 useRef 的搭配
在 React 19 之前,如果我們想將 ref 傳給從父層往下傳遞,需要搭配 forwardRef 使用,如果單純當作 props 往下傳是沒有用的,例如這樣**:**
// ❌ 錯誤使用方式
function Parent() {
const pRef = useRef(null);
return <Child ref={pRef} />
}
function Child({ref}) {
return <p ref={ref}>Hello World</p>
}
這個時候需要搭配 forwardRef
將 Ref 傳遞給子組件:
// ✅ 正確使用方式
function Parent() {
const pRef = useRef(null);
return <Child ref={pRef} />
}
const Child = forwardRef((props, ref) => {
return <pRef ref={ref}>Hello World</p>
})
不過在 React 19 之後,就不需要使用 forwardRef
了,直接傳 ref 就好,但維護舊專案時就要注意了。
如何在 TypeScript 中正確使用 useRef
在使用 TypeScript 時,useRef
也需要一些額外的注意,尤其在型別註記和初始值方面。
為 DOM 元素設定正確的型別:當我們用 useRef
來存放 DOM 元素的引用時,要在泛型中指定對應的元素型別。例如,HTMLInputElement
、HTMLDivElement
等。
通常我們會將初始值設為 null
,因為在尚未掛載前沒有 DOM 元素可引用。例如:
const inputRef = useRef<HTMLInputElement>(null);
這樣,inputRef.current
的型別就會是 HTMLInputElement | null
。
結語
我們詳細介紹了 React useRef
Hook 的用法。對初學者而言,重點在於了解 useRef
可以跨渲染儲存資料且不會引起重渲染,常常用來訪問 DOM 或保存一些輔助性資訊。
所以什麼資料適合放進 ref,什麼資料該用 state,很重要,使用的很可以更優化效能。