useRef 入門:如何存資料又不觸發重新渲染?
更新日期: 2025 年 4 月 18 日
在 React 中,useState
是大家學得最多也用得最熟的 Hook。
它能讓元件擁有「狀態」,一但你用 setState
更新資料,React 就會重新渲染畫面,這樣我們才能看到最新的變化。
但你有沒有遇過這種情境:
- 我只想記住一個資料(例如:上一次按鈕點擊的時間),根本不需要重新渲染畫面。
- 我想要在畫面一開始顯示時,自動把焦點放在某個 input 欄位上。
這些需求用 useState
處理不太合適,因為它會觸發重新渲染。
而這時,我們就可以使用另一個 Hook —— useRef
!
什麼是 useRef?
它其實就是一個「可以記住資料」的小容器,不會引發重新渲染
在 React 中,useRef
是一個萬用的「資料容器」,專門用來儲存那些你想記住,但又不希望改變它就觸發畫面更新的資訊。
你可以把它想像成一張小紙條🏷️ ,貼在元件的背後,在元件的整個生命週期中,React 都會幫你保存這張小紙條,不管你 render 幾次,它都不會重設。
useRef
是 React 提供的一個 Hook,用來建立一個「可持久存在的參考物件」(reference object)。
這個物件只有一個屬性:.current
你可以把任何值(數字、字串、物件、函式、DOM 元素……都行)放進 .current
裡,它會整個元件的生命週期中一直保留,就像是一個不會被重新渲染清空的隱藏儲藏箱。
語法與基本用法
import { useRef } from 'react';
function Example() {
const countRef = useRef(0); // 建立一個 ref,初始值為 0
console.log(countRef.current); // ➜ 印出 0
countRef.current = 5; // 手動修改內容
console.log(countRef.current); // ➜ 印出 5
return <div>看看 console.log</div>;
}
🧠 .current
是什麼?
當你呼叫 useRef(initialValue)
時,React 會回傳一個這樣的物件:
{
current: initialValue
}
所以:
const countRef = useRef(0);
// 這其實等同於:
const countRef = { current: 0 };
你可以隨時更新 countRef.current
的值,但這個動作 不會觸發畫面重新渲染,因為 React 根本沒「監控」它的變化。
📦 可以想像成一個小抽屜
想像一下你有一個抽屜(ref
),裡面可以放東西(current
),這些東西:
- 會一直被記住
- 可以在任何時候取出或放進去
- 但你打開抽屜、改裡面的東西,畫面是不會改變的
這就是 useRef
的精神。
和 useState
最大的差異是什麼?
雖然 useState
和 useRef
都能用來「儲存資料」,但它們的行為邏輯、更新方式,以及使用目的完全不同。
差異對照表一覽
特性 | useState | useRef |
---|---|---|
是否會觸發重新渲染 | ✅ 是的,狀態變動就會重新 render | ❌ 否,更新 .current 不會重新 render |
初始語法 | const [count, setCount] = useState(0) | const countRef = useRef(0) |
更新方式 | setCount(newVal) | countRef.current = newVal |
使用場景 | 畫面需要因資料改變而更新 | 資料僅作為記憶用途,不影響畫面 |
背後邏輯 | 透過 state → 讓 React 自動重畫 UI | 只是建立一個在元件生命週期中持久存在的容器 |
✅ 簡單記憶口訣
- 需要讓畫面更新 👉 用
useState
- 只想記住資料,不影響畫面 👉 用
useRef
實際對比:計數器範例
👉 用 useState
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>+1</button>
<p>你點了 {count} 次</p>
每點一次按鈕,畫面就會重新渲染 →
count
的值會顯示在畫面上。
👉 用 useRef
const countRef = useRef(0);
<button onClick={() => {
countRef.current += 1;
console.log(countRef.current);
}}>+1</button>
點按鈕時,雖然資料有更新,但畫面不會變動,因為
countRef.current
改變 不會觸發重新渲染。你只能從 console 看到值變了。
形象化比喻:useState
vs useRef
🟢 useState
就像是一個 會自動更新顯示螢幕的收銀機顯示板
- 你按下按鈕(
setState()
)→ 內部數字改變 - 螢幕上的數字也會自動更新顯示出來
- 使用者可以立刻看到畫面上的變化
✅ 換句話說:
useState
不只是記資料,它同時會讓畫面跟著改變
🟡 useRef
就像是一張 只有你自己能看到的小備忘錄
- 你可以在上面記筆記(
ref.current = value
) - 這張紙條不會貼在畫面上 → 使用者完全看不到
- 但你可以在任何時候打開來看,或在程式中讀取
✅ 換句話說:
useRef
是開發者自己的小抄,不影響畫面,只給自己看
對照圖表:
比喻物件 | 行為說明 | 對應 Hook |
---|---|---|
收銀機的螢幕 | 改變金額 → 畫面會跟著更新顯示 | useState |
店員手上的便條紙 | 記錄今天有幾個人來問優惠 → 不秀給客人看 | useRef |
useRef
的三大應用場景詳細解析
記住不影響畫面的資料
有些資料你只是想「記錄下來」,日後某個動作或副作用會用到,但它本身不影響 UI 呈現,也不應該觸發畫面重新渲染。
如果你用 useState
儲存這些資料,每次更新都會造成元件重新 render,反而多餘又浪費效能。
以下是常見的例子:
✅ 上一次點擊的時間
你想要偵測「兩次點擊間隔是否太短」,但這只是邏輯判斷,不需要改變畫面:
const lastClickTimeRef = useRef(Date.now());
const handleClick = () => {
const now = Date.now();
if (now - lastClickTimeRef.current < 500) {
alert("點太快了!");
}
lastClickTimeRef.current = now;
};
✅ API 呼叫中的 loading flag(不顯示在畫面上)
你可能會同時發出多個 API,但你只需要用一個 flag 確保「目前是否有請求在執行中」,不一定要顯示 loading:
const isRequestingRef = useRef(false);
const fetchData = async () => {
if (isRequestingRef.current) return;
isRequestingRef.current = true;
await fetch(...);
isRequestingRef.current = false;
};
🧠 這段程式碼的邏輯設計是什麼?
它的目的是:
✅ 避免在非同步操作進行中,重複觸發同一個動作(像是多次送出請求)
常見使用場景:
- 使用者狂點按鈕
- 無限滾動觸發多次請求
- 表單誤觸兩次送出
🔁 運作邏輯分解(照「意圖」順序說明)
1️⃣ 建立一個 ref
當作旗標
const isRequestingRef = useRef(false);
- 我們需要有一個「請求中」的標記變數
useRef()
可以建立一個「元件生命週期中持久存在」的資料容器- 它不像
useState
,不會導致元件重渲染,適合拿來記錄邏輯狀態
2️⃣ 設定旗標為 true
,表示「我現在開始請求了」
isRequestingRef.current = true;
- 一旦請求開始,就把旗標打開
- 用來告訴未來可能再次觸發的動作:「我正在忙,別再來!」
3️⃣ 執行非同步操作(如發送 API)
await fetch(...);
- 做真正的工作:發送資料、取得資料等等
- 這段期間內,我們不希望再重複觸發
fetchData
4️⃣ 請求完成後,把旗標設回 false
isRequestingRef.current = false;
- 表示我已經「閒下來了」,下次可以再請求
- 重設這個值,就能恢復請求權限
5️⃣ 下一次呼叫時,先檢查旗標
if (isRequestingRef.current) return;
- 這是「門神」:擋住重複觸發
- 如果旗標是
true
,代表上一個請求還沒完成,就直接 return,不做任何事
🧾 整體邏輯一句話
這段程式碼的設計是:
建立一個「請求中」的旗標,在請求開始前打開、請求完成後關閉,並在每次請求前先檢查旗標,達到防止重複發送的效果。
✅ 最終範例總結
const isRequestingRef = useRef(false); // 建立一個旗標(不影響畫面)
const fetchData = async () => {
if (isRequestingRef.current) return; // 檢查是否請求中,若是就擋掉
isRequestingRef.current = true; // 標記為請求中
await fetch(...); // 執行真正的請求
isRequestingRef.current = false; // 請求完成後重設旗標
};
✅ 計時器 ID / WebSocket 連線物件 / 滾動位置
這些值屬於「非視覺化的程式資料」,不應該造成 re-render,但需要在元件中持久存在:
const timerIdRef = useRef(null);
const socketRef = useRef(null);
const scrollPositionRef = useRef(0);
這些狀況下,用 useRef
記錄,就能保留資料、不中斷生命週期,也不會浪費資源去重繪畫面。
取得某個 DOM 元素的控制權
你可能會想問:
「取得 DOM 元素」這件事感覺不像是「記資料」,為什麼也是
useRef
的使用場景?
其實在 React 中,我們不建議直接使用 document.querySelector()
去操作 DOM,因為這會繞過 React 的 Virtual DOM 機制,導致資料與畫面不一致,也不利於元件封裝。
這時,React 提供 ref
系統搭配 useRef()
,讓你安全地記住某個 DOM 元素的參考(reference),並在 useEffect
中對它進行操作。
✅ 範例:讓 input 自動聚焦
import { useRef, useEffect } from "react";
function InputFocus() {
const inputRef = useRef(null); // 建立 ref,準備放 input 元素
useEffect(() => {
// 元件掛載後,自動聚焦 input 欄位
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="請輸入文字..." />;
}
完整解釋邏輯👇
✅ 程式碼邏輯執行順序一覽
import { useRef, useEffect } from "react";
function InputFocus() {
const inputRef = useRef(null); // 👉 步驟 1:建立 ref
useEffect(() => {
// 👉 步驟 3:在畫面「掛載完成」後,做副作用處理
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="請輸入文字..." />; // 👉 步驟 2:綁定 ref
}
🪜 步驟 1:建立 ref
容器(程式啟動時)
const inputRef = useRef(null);
useRef(null)
回傳一個物件{ current: null }
- 這個
inputRef
是用來「接住」將來會被 React 塞進來的 DOM 元素
此時 inputRef.current === null
🪜 步驟 2:render 畫面 → JSX 裡的 ref={inputRef}
被解析
return <input ref={inputRef} placeholder="請輸入文字..." />;
在 React 裡,ref 是一個保留屬性(special attribute)
當你這樣寫:
<input ref={inputRef} />
React 在背後會自動做這件事:
inputRef.current = 對應的 DOM 元素
也就是說:
React 看到 ref 是一個用
useRef()
建立的物件,就會在 render 完成後,自動把對應的 DOM 元素放進.current
裡面。
📦 更技術一點的說法
當 React 渲染這個 input 元素時,它會:
- 看見
ref
傳入了一個物件(像是useRef()
回傳的那種) - 在 DOM 掛載(mount)時,把 DOM 節點(這個
<input>
)放進這個物件的.current
- 在 DOM 卸載(unmount)時,把
.current
設為null
這是 React 幫你做的特殊處理,你平常學 JSX 時不會聽到「ref 的 {} 有特殊功能」——因為 只有 ref
這個屬性有這樣的魔法行為。
✅ 重新用一句話說清楚
這行的意義是:
告訴 React:「這個 input 元素對應的 DOM 節點,請幫我自動存進
inputRef.current
裡。」
React 看到你把一個 ref 物件傳進 ref
屬性,就會幫你把 input 的 DOM 結構存進去。
🪜 步驟 3:useEffect(() => {...}, [])
執行(畫面掛載後)
useEffect(() => {
inputRef.current.focus(); // 這時 inputRef.current 已經有值了
}, []);
- 當元件第一次掛載完成,
useEffect
才會執行(因為是空依賴陣列) - 這時候
inputRef.current
已經不是 null,而是 input 的 DOM 元素了 - 所以
.focus()
這一行會順利執行,讓游標自動跳進輸入框
記錄最新狀態,避免閉包陷阱(進階用途)
React 中的閉包(closure)有時會讓你「抓不到最新的 state」,特別是在 setInterval
、setTimeout
、訂閱 callback 等非同步邏輯裡。
這時,useRef
是你最可靠的保險箱。
✅ 範例:setInterval 裡拿不到最新的 state?
function Timer({ count }) {
const latestCount = useRef(count); // 初始化
// 每次 render 時更新最新值
useEffect(() => {
latestCount.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log("最新 count:", latestCount.current); // 永遠是最新
}, 1000);
return () => clearInterval(id);
}, []);
return <p>目前 count: {count}</p>;
}
🎯 設計思路:我們要解決什麼問題?
想像情境:
你有一個畫面每秒會 console.log
一次最新的 count
,這個 count
是從外部 props 傳進來的。
你可能會天真地這樣寫:
useEffect(() => {
const id = setInterval(() => {
console.log("count: ", count);
}, 1000);
}, []);
❗結果:它只會 log 初始值,永遠不變!
因為這段 setInterval
只在第一次 render 被建立,而裡面用到的 count
,其實是當時 render 時的快照(閉包),它之後不會更新。
👉 我們遇到了「閉包陷阱」(closure trap)
✅ 設計目標:我想要每秒 log 出最新的 count!
🧩 解法設計流程
🔹1. 問題在於 setInterval 裡的 count 不會更新
那我要想辦法把「最新的 count」放進一個我可以隨時讀取的容器裡。
🔹2. 我不要用 state(因為會造成重渲染)
我不是要更新畫面,只是邏輯上要用到最新值,所以用 useRef
剛好最適合。
🔹3. 每次 render 時,把最新的 count 存進 ref 裡
useEffect(() => {
latestCount.current = count;
}, [count]);
這段的作用是:
不管外部傳進來的 count 是多少,只要有變化,我就把它更新到
latestCount.current
裡。
🔹4. 在 setInterval 裡,讀取的是 latestCount.current
setInterval(() => {
console.log("最新 count:", latestCount.current);
}, 1000);
這樣就可以永遠拿到最新的 count,避免被閉包鎖死!
✅ 最終完整程式邏輯說明
function Timer({ count }) {
const latestCount = useRef(count); // 🔸 建立一個 ref,記錄最新 count 值
useEffect(() => {
// 🔸 每次 count 改變,就更新 ref 裡的值(但不觸發畫面重渲染)
latestCount.current = count;
}, [count]);
useEffect(() => {
// 🔸 設定一個定時器,每秒 log 一次最新的 count
const id = setInterval(() => {
console.log("最新 count:", latestCount.current);
}, 1000);
return () => clearInterval(id); // 🔸 清除定時器(元件卸載或重建時)
}, []);
return <p>目前 count: {count}</p>;
}
✅ 一句話總結
這段程式的設計目的是:「讓非同步或延遲執行的邏輯,可以一直拿到最新的狀態,不被初始閉包鎖死」,而
useRef
就是關鍵武器。
小結
類型 | 說明 | 為何用 useRef ? |
---|---|---|
📋 暫存資料 | 計時器 ID、flag、上次點擊時間 | 不觸發 render,只是記錄 |
🧩 DOM 操作 | focus、scroll、canvas、影片控制 | 需要 DOM 參考,不影響畫面 |
🔁 閉包陷阱 | setInterval、非同步中抓不到最新值 | ref 保持同步,不會卡住 |
結語
useRef
是 React Hook 中最被低估的一員,它能:
- 存資料但不重新渲染(記錄上次點擊、表單狀態)
- 存取 DOM 元素(自動聚焦、操作 Canvas、控制影片)
理解它之後,你會發現很多「不需要畫面變動」的邏輯,都能用它來處理,不僅簡單,也更高效!