前端框架
-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,很重要,使用的很可以更優化效能。