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 最大的差異是什麼?

雖然 useStateuseRef 都能用來「儲存資料」,但它們的行為邏輯、更新方式,以及使用目的完全不同

差異對照表一覽

特性useStateuseRef
是否會觸發重新渲染✅ 是的,狀態變動就會重新 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 元素時,它會:

  1. 看見 ref 傳入了一個物件(像是 useRef() 回傳的那種)
  2. 在 DOM 掛載(mount)時,把 DOM 節點(這個 <input>)放進這個物件的 .current
  3. 在 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」,特別是在 setIntervalsetTimeout、訂閱 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、控制影片)

理解它之後,你會發現很多「不需要畫面變動」的邏輯,都能用它來處理,不僅簡單,也更高效!

Similar Posts