探索 React Hook —— useRef:狀態與參考的選擇、DOM 操作,以及其他常見用法

zxlee-avatar-img
發佈於React
更新 發佈閱讀 19 分鐘

在先前探索 useState 的文章裡,我們討論過當其保存的「狀態」值更新時,會觸發元件的重新渲染;在 React Hook 裡,提供了另一個同樣作為保存資料的工具 useRef,我們將它儲存的資料稱為「參考」(reference)。

參考和一般宣告的變數一樣,資料改變時不會觸發元件的重新渲染,不過,不同於一般變數在每次渲染後都必須重新計算,參考能夠在跨渲染之間保存原有的值。

如何使用 useRef

useRef 的語法很簡單,首先,跟 useState 一樣,要給它定義初次渲染時的初始值;接著,useRef 會回傳一個參考物件,這個物件只有一個 current 屬性,你所定義的初始值,在元件初次渲染時便是賦予給這個屬性:

const ref = useRef(initialValue);

console.log(ref); // { current: initialValue }

ref 就只是單純的 JavaScript 物件,current 的值可以是任意型別的資料。 而 useRef 的作用在於:React 確保我們能夠在整個元件生命週期中,保持同一個 ref 的物件引用。

比較 useStateuseRef:如何選擇狀態與參考?

相較於狀態是不可變的,你需要靠 useState 回傳的 setState 方法來覆寫、更新狀態,這個動作會觸發元件的重新渲染;相對的,參考是可變的,你可以把新的值直接賦予給 current 屬性,React 並不會追蹤 current 的變化,元件也不會因此重新渲染。

不過,更重要的是,在每次重新渲染後,React 都會回傳同一個 ref 物件,因此無論 ref.current 的值如何變化,元件會一直記住這些變動。因此,ref 比較適合存放「與畫面無關、但需要被記住」的資訊,例如 DOM 節點、計數器 ID、第三方函式庫實例等等。

以下是我在 CodePen 所寫的範例,在這個計數器元件中,我分別用狀態與參考定義數字,並在同一個畫面呈現:

🌟 備註:為方便告知點擊後發生了什麼狀況,我在兩者的事件處理器都加入了瀏覽器的 alert,如果不喜歡 alert 阻斷測試的流暢度,可以自行點進 CodePen,將它們拿除。

畫面中,State 後方的數字是用 useState 所定義,並在點擊 State + 1 後,用 setState 更新狀態;

// 定義狀態
const [stateCount, setStateCount] = useState(0);

// 事件處理器
const handleStateClick = () => {
setStateCount((c) => c + 1);
alert("將執行重新渲染,將一併渲染出 State 跟 Ref 的最新值");
};

// JSX
<p>State:{stateCount}</p>
<button onClick={handleStateClick} type="button">
State + 1
</button>

而 Ref 後方的數字則是用 useRef 所定義,點擊 Ref + 1 後,讓其 current 值累加:

// 定義參考
const refCount = useRef(0);

// 事件處理器
const handleRefClick = () => {
refCount.current++;
alert(`Ref 的 current 值已更新為 ${refCount.current} 👉 但畫面將不會更新`);
};

// JSX
<p>Ref:{refCount.current}</p>
<button onClick={handleRefClick} type="button">
Ref + 1
</button>

從這些範例的程式碼,可以發現:

當點擊 Ref + 1 的按鈕,並沒有發生元件渲染,因此縱使 refCount.current 的值確實改變了,但畫面上的數字依然停留在當次渲染的值;直到點擊 State + 1 的按鈕,因為 setStateCount 的執行觸發了重新渲染,所以會立即更新畫面上的數字,包括 refCount.current 的最新值。

也就是說,顯示在畫面的參考值,因為本身的變化不觸發重新渲染,必須依賴另外一個重新渲染發生才會更新顯示,如果要依資料變動而更新畫面,何不用狀態定義就好呢?因此,我們通常會避免把參考顯示在畫面上,所以,當你需要在狀態與參考之間選擇,可以依據一個原則:

「會影響畫面的資料 → 用 useState 定義;
不會影響畫面的資料 → 用 useRef 定義。」

useRef 常見用法

一、跨渲染之間保存不影響畫面的資料

useRef 最直觀的用法,便是運用它的特性,在跨渲染間保存不影響畫面的資料。其中計數器 API 的識別碼便是最典型的例子:

由於計數器 API (setIntervalsetTimeout)不會隨元件的卸載而自動清除,所以我們需要儲存它們回傳的識別碼(正整數)作為相應清除方法(clearIntervalclearTimeout)的參數。

它們的識別碼不會用於畫面,因此透過 useRef 來儲存是較推薦的做法,我們將會執行以下動作來實作一個碼錶元件:

  1. useState 管理畫面上顯示的時間秒數及碼錶狀態
  2. setInterval 讓時間每秒遞增
  3. useRef 儲存 interval ID,以便讓副作用及事件處理器管理

以下是實作步驟:

⓵ 初始化狀態與參考,並顯示在 UI

function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);

return (
<div>
<h1>碼錶:{time}</h1>
<button disabled={isRunning} type="button">
開始
</button>
<button disabled={!isRunning} type="button">
暫停
</button>
<button type="button">重置</button>
</div>
)
}

⓶ 實作「開始」、「暫停」及「重置」碼錶的事件處理器

// 開始計時
function handleStart() {
if (!isRunning) return;

setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime(t => t + 1); // 👈 1. 要使用函式更新
}, 1000);
};

// 暫停計時
function handleStop() {
setIsRunning(false);
clearInterval(intervalRef.current); // 👈 2. 清理計數器
};

// 重置計時
function handleReset() {
setTime(0);
setIsRunning(false);
clearInterval(intervalRef.current); // 👈 2. 清理計數器
};
1. 如果此處使用 setTime(time + 1) 會產生閉包問題,導致 time 會持續停留在初始值,必須使用 setState 的函式更新模式才能正確運作
2. 在處理暫停及重置事件時,記得都要清理計數器

⓷ 用副作用處理元件卸載時的清理計數器

為避免元件因為被切換路由或條件渲染移除,而沒有清除 interval 計數器的問題,我們需要在副作用上追加清理函式:

useEffect(() => {
return () => {
clearInterval(intervalRef.current);
};
}, []);

㊣ 將事件處理器綁定後的 Demo:

不過,以這個例子來說,讓副作用統一來處理 interval 的訂閱與清除可能是更好的實踐:

useEffect(() => {
if (!isRunning) return;
const id = setInterval(() => {
setTime((t) => t + 1);
}, 1000);

return () => {
clearInterval(id);
};
}, [isRunning]); // 👈 透過切換 isRunning 來重啟副作用

我們將 isRunning 放入 useEffect 的依賴陣列中來控制副作用的重啟;如此,事件處理器只需轉換 isRunning 的狀態即可:

function handleStart() {
setIsRunning(true);
}

function handleStop() {
setIsRunning(false);
}

function handleReset() {
setTime(0);
setIsRunning(false);
};

Demo:

比起直接在事件處理器以「命令」的方式處理計時器的訂閱與清除,寫法反覆、不易管理外還容易漏掉計數器清理,甚至可能會因為 React「批次處理狀態」的做法,在某些極端狀況產生問題;事實上,第二種寫法則更接近 React 宣告式 UI 的思維模式——透過狀態的切換,決定計時器的訂閱與清理。

在第二種寫法裡,也可以在元件最外層讓 useRef 儲存計數器 ID,並在副作用的其他地方使用,不過在這個例子中並不需要。

二、取得並操作 DOM 節點

上面有提及,React 是透過宣告的方式來創建介面 (declarative UI),我們根據不同的狀態,來描述相應的介面。但是,操作 DOM 並沒有辦法透過狀態描述。React 的元件有生命週期、有虛擬 DOM,用 JavaScript 原生手法 querySelector 操作的話可能會導致許多問題,因此 React 讓 useRef 成為與 DOM 溝通的橋樑。

要用 useRef 操作 DOM,主要有三個步驟:

  1. useRef(null) 初始化參考變數
  2. 在 JSX 透過 ref 屬性,綁定要取得參考的 DOM 元素
  3. 副作用或是事件處理器取得該 DOM 元素的屬性及方法

比如說,載入畫面時自動對焦的輸入框:

function AutoFocusInput(){
// 1. 用 useRef(null) 初始化變數
const inputRef = useRef(null);

useEffect(() => {
// 3. 在副作用取得 input 方法
inputRef.current?.focus();
}, [])

// 2. 綁定 DOM 元素
return <input ref={inputRef} type="text" />
}

我們以 null 來初始化參考 inputRef,元件掛載完成之後,React 會透過 ref 屬性將對應的 DOM 元素賦值給 inputRef.current;當此節點被移除,React 會將 current 屬性的值設定回 null,讓 DOM 的處理與元件生命週期同步。

三、存取上次渲染的狀態、props

我們可以在副作用中,將當前狀態或 props 傳給 ref.current,來儲存此次渲染的狀態。這個作法的原理是利用 useEffect 會在元件渲染之後運作,以及 useRef 更新後不會觸發重新渲染的特性,就能在檯面下默默儲存狀態:

=== 元件初始渲染 ===
👉 元件函式執行,註冊 useRef、useState 初始值
👉 用 State 初始值渲染畫面
👉 useEffect 執行,將 State 初始值賦予給 Ref.current
👉 等待 State 更新,重新渲染畫面...

=== State 更新後,元件重新渲染 ===
👉 元件函式重新執行
👉 從 Ref.current 拿到 State 初始值
👉 用新 State 渲染畫面
👉 useEffect 執行,將新 State 賦予給 Ref.current
👉 等待 State 更新,重覆此步驟

能夠拿到上一個狀態之後,就能用新狀態(當次渲染的狀態)來做比較,比如說:

const [price, setPrice] = useState(168);
const prevPriceRef = useRef(null);

useEffect(() => {
// 將當次渲染的 price 存起來,作為下次渲染比較的依據
prevPriceRef.current = price;
}, [price]);

// 計算價差
const difference =
prevPriceRef.current !== null ? price - prevPriceRef.current : 0;

// ...

像這樣,就可以根據比較前後狀態所產生的數據,來改變畫面的樣式、敘述等等,比如說:

注意事項

一、避免參考用於畫面更新

正如前文所述,參考不會觸發元件的重新渲染,因此將其用於畫面更新容易發生不預期的狀況。

function Counter() { 
const countRef = useRef(0); 

function handleIncrement() { 
countRef.current++; // ⚠️ 不會觸發畫面更新
} 

return ( 
<div> 
<p>計數:{countRef.current}</p>
<button onClick={handleIncrement}>增加</button> 
</div> 
); 
};

二、在渲染時直接讀取或修改參考的 current 值

React 的核心原則是「渲染階段必須是純函式 (pure function)」。因此,在渲染階段必須避免出現副作用以及可變資料的讀取或修改

而參考的 current 值便屬於可變資料,既不會被 React 追蹤,也不會觸發重新渲染,因此容易發生不預期的狀況。

比如說之前範例的自動對焦輸入框,如果這樣寫的話:

function AutoFocusInput() {
const inputRef = useRef(null);

if (inputRef.current) {
inputRef.current.focus();
};

return <input ref={inputRef} />;
};

初次渲染時,在 DOM 還沒有建立前就讀取 inputRef.current,所以它的值將會是 null。因此條件不成立,當輸入框被建立時就不會有對焦效果。

假如說這個元件裡狀態改變發生,觸發重新渲染時,此時 DOM 已經存在,所以 if 條件成立,觸發對焦,但是新的提交階段 (commit) 尚未完成,可能導致不預期的渲染結果。因此需要把這段邏輯放在副作用中,確保在提交階段之後、DOM 已經存在時執行。

function AutoFocusInput() {
const inputRef = useRef(null);

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
};
}, []);

return <input ref={inputRef} />;
};

總結

useRef 就像 React 元件的秘密口袋:

  1. 它能夠保存資料,但不會跟狀態一樣觸發重新渲染
  2. 在重新渲染之後依然能夠存取到上次的資料,不像一般變數會重置
  3. 最常用於取得 DOM 元素,操作其原生的 API
  4. 適合儲存不影響畫面的資料,像是計時器 ID、訂閱的物件實例

參考資料

  1. React 官方文件:useRef
  2. Web Dev Simplified:Learn useRef in 11 Minutes

延伸閱讀

  1. 探索 React Hook——useState(),掌握元件狀態管理的核心
  2. 探索 React Hook —— useEffect:掌握副作用的用法、陷阱與替代方案
留言
avatar-img
肝 code 人生
1會員
9內容數
2024 年 7 月開始的「肝 code 人生」,2025 年 1 月撰寫第一篇程式筆記
肝 code 人生的其他內容
2026/02/03
本文將深入探討 React 的 useEffect Hook,從其基本定義、常見用法,到開發者常踩的陷阱。此外,文章也將介紹新的 useEffectEvent Hook,以及提供避免 useEffect 陷阱的實用技巧。
2026/02/03
本文將深入探討 React 的 useEffect Hook,從其基本定義、常見用法,到開發者常踩的陷阱。此外,文章也將介紹新的 useEffectEvent Hook,以及提供避免 useEffect 陷阱的實用技巧。
2026/01/07
本文探討 React 中 `useState()` Hook 的核心功能,解析狀態的定義、React Hooks 的使用規範,並透過計數器元件的實例,詳細說明 `useState()` 的基本用法、初始狀態設定、何時應使用狀態而非一般變數,以及物件陣列的更新與透過 `key` 重置狀態等進階技巧。
2026/01/07
本文探討 React 中 `useState()` Hook 的核心功能,解析狀態的定義、React Hooks 的使用規範,並透過計數器元件的實例,詳細說明 `useState()` 的基本用法、初始狀態設定、何時應使用狀態而非一般變數,以及物件陣列的更新與透過 `key` 重置狀態等進階技巧。
2025/04/20
這篇文章教學如何在 React 中建立一個檔案上傳元件,包含檔案選擇、資訊顯示、狀態管理、即時進度回饋等功能,並使用 Axios 處理檔案上傳。
Thumbnail
2025/04/20
這篇文章教學如何在 React 中建立一個檔案上傳元件,包含檔案選擇、資訊顯示、狀態管理、即時進度回饋等功能,並使用 Axios 處理檔案上傳。
Thumbnail
看更多
你可能也想看
Thumbnail
這篇文章分享了申請 AppWorks School 轉職培訓的過程,包括背景及源起、報名前準備、報名及面談、總結等內容,並提供了未來申請者的建議。如果你正在尋求轉職機會,這篇文章可能對你有所幫助。
Thumbnail
這篇文章分享了申請 AppWorks School 轉職培訓的過程,包括背景及源起、報名前準備、報名及面談、總結等內容,並提供了未來申請者的建議。如果你正在尋求轉職機會,這篇文章可能對你有所幫助。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
Create project 裝好 vue CLI 之後,只要在終端輸入 vue create 想取的專案名稱 就可以開始創建 vue 專案囉 1. Pick a preset 2. Select the features 3. Choose the version of vue 選擇 vue3
Thumbnail
Create project 裝好 vue CLI 之後,只要在終端輸入 vue create 想取的專案名稱 就可以開始創建 vue 專案囉 1. Pick a preset 2. Select the features 3. Choose the version of vue 選擇 vue3
Thumbnail
作為一個非常不專業的前端初學者,有陣子常常卡在公司官網,要插入 Youtube 影片無法RWD(響應式)的問題。 跟不熟悉 網頁技術的朋友們介紹,RWD 就是指網頁的排版能跟著螢幕的大小縮放、變化編排,在這個人手一機的時代,特別重要。
Thumbnail
作為一個非常不專業的前端初學者,有陣子常常卡在公司官網,要插入 Youtube 影片無法RWD(響應式)的問題。 跟不熟悉 網頁技術的朋友們介紹,RWD 就是指網頁的排版能跟著螢幕的大小縮放、變化編排,在這個人手一機的時代,特別重要。
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
為什麼 JavaScript 中的字串無法修改?為什麼複製變數後,改變其中一個會影響另一個?本文將深入解析 JavaScript 的原生值 (primitive values) 與物件 (objects) 的運作機制,說明傳值與傳參考的差異,避免非預期的程式行為,提升開發效率!!
Thumbnail
為什麼 JavaScript 中的字串無法修改?為什麼複製變數後,改變其中一個會影響另一個?本文將深入解析 JavaScript 的原生值 (primitive values) 與物件 (objects) 的運作機制,說明傳值與傳參考的差異,避免非預期的程式行為,提升開發效率!!
Thumbnail
iFrame在微前端架構中的優缺點,分析了其對用戶體驗、SEO及性能的影響。 iFrame在嵌入小型UI元件、創建隔離環境等特定場景中仍具備相當的應用價值。
Thumbnail
iFrame在微前端架構中的優缺點,分析了其對用戶體驗、SEO及性能的影響。 iFrame在嵌入小型UI元件、創建隔離環境等特定場景中仍具備相當的應用價值。
Thumbnail
data-* attributes 是 HTML 內建的屬性,可將網頁的狀態與元素進行綁定。而Tailwind CSS 在 3.2 版更新中推出使用 data-* attributes 自訂樣式的功能,讓樣式設定可以更動態多變。
Thumbnail
data-* attributes 是 HTML 內建的屬性,可將網頁的狀態與元素進行綁定。而Tailwind CSS 在 3.2 版更新中推出使用 data-* attributes 自訂樣式的功能,讓樣式設定可以更動態多變。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
在剛開始寫 JavaScript 可能大多數的人不會特別意識到 JavaScript 的型別系統有什麼特別之處,我是在看完 Youtube 上 CS50 的課程,才理解到在不同的程式語言中,會因為語言的特性而有不同的系統,JavaScript 就是偏向比較特別的那一種。
Thumbnail
在剛開始寫 JavaScript 可能大多數的人不會特別意識到 JavaScript 的型別系統有什麼特別之處,我是在看完 Youtube 上 CS50 的課程,才理解到在不同的程式語言中,會因為語言的特性而有不同的系統,JavaScript 就是偏向比較特別的那一種。
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News