React useRef

useRef 是一個可以讓你保存不需要觸發重新渲染的值的 Hook。它有兩個主要用途:

  1. 存取 DOM 元素:取得實際的 DOM 元素參照
  2. 保存可變值:保存不會觸發重新渲染的值

基本語法

import { useRef } from 'react'

const ref = useRef(initialValue)

useRef 返回一個物件,有一個 current 屬性:

{
  current: initialValue
}

存取 DOM 元素

最常見的用途是取得 DOM 元素的參照,例如讓輸入框獲得焦點:

import { useRef } from 'react'

function TextInputWithFocusButton() {
  // 建立一個 ref
  const inputRef = useRef(null)

  function handleClick() {
    // 透過 ref.current 存取 DOM 元素
    inputRef.current.focus()
  }

  return (
    <div>
      {/* 將 ref 附加到 DOM 元素 */}
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>聚焦輸入框</button>
    </div>
  )
}

流程說明:

  1. 使用 useRef(null) 建立一個 ref
  2. 將 ref 傳遞給元素的 ref 屬性
  3. React 會將 DOM 元素存入 ref.current
  4. 之後就可以透過 ref.current 存取 DOM 元素

常見 DOM 操作範例

聚焦輸入框

function SearchInput() {
  const inputRef = useRef(null)

  // 元件掛載後自動聚焦
  useEffect(() => {
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} placeholder="搜尋..." />
}

滾動到特定位置

function ScrollToTop() {
  const topRef = useRef(null)

  function scrollToTop() {
    topRef.current.scrollIntoView({ behavior: 'smooth' })
  }

  return (
    <div>
      <div ref={topRef}>頁面頂端</div>
      {/* 很多內容... */}
      <button onClick={scrollToTop}>回到頂端</button>
    </div>
  )
}

操作影片播放

function VideoPlayer() {
  const videoRef = useRef(null)
  const [isPlaying, setIsPlaying] = useState(false)

  function togglePlay() {
    if (isPlaying) {
      videoRef.current.pause()
    } else {
      videoRef.current.play()
    }
    setIsPlaying(!isPlaying)
  }

  return (
    <div>
      <video ref={videoRef} src="/video.mp4" />
      <button onClick={togglePlay}>{isPlaying ? '暫停' : '播放'}</button>
    </div>
  )
}

保存可變值

除了 DOM 操作,useRef 還可以用來保存不需要觸發重新渲染的值:

保存前一個值

function Counter() {
  const [count, setCount] = useState(0)
  const prevCountRef = useRef(0)

  useEffect(() => {
    prevCountRef.current = count
  }, [count])

  return (
    <div>
      <p>
        現在: {count}, 之前: {prevCountRef.current}
      </p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

保存計時器 ID

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

  function start() {
    setIsRunning(true)
    intervalRef.current = setInterval(() => {
      setTime((prev) => prev + 1)
    }, 1000)
  }

  function stop() {
    setIsRunning(false)
    clearInterval(intervalRef.current)
  }

  function reset() {
    stop()
    setTime(0)
  }

  // 清理計時器
  useEffect(() => {
    return () => clearInterval(intervalRef.current)
  }, [])

  return (
    <div>
      <p>時間: {time} 秒</p>
      {isRunning ? <button onClick={stop}>停止</button> : <button onClick={start}>開始</button>}
      <button onClick={reset}>重置</button>
    </div>
  )
}

追蹤渲染次數

function RenderCounter() {
  const renderCount = useRef(0)

  // 每次渲染時增加計數(不會觸發重新渲染)
  renderCount.current += 1

  return <p>這個元件已渲染 {renderCount.current} 次</p>
}

useRef vs useState

特性useRefuseState
改變值會觸發重新渲染❌ 不會✅ 會
值在渲染間保持✅ 是✅ 是
可以直接修改✅ 是❌ 否(要用 setter)
存取方式ref.current直接使用

使用原則:

  • 需要觸發重新渲染 → 用 useState
  • 不需要觸發重新渲染 → 用 useRef

傳遞 ref 給子元件

在 React 中,你可以直接將 ref 作為 prop 傳遞給子元件(從 React 19 開始不再需要 forwardRef):

// 子元件直接接收 ref
function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

// 父元件
function Form() {
  const inputRef = useRef(null)

  function handleClick() {
    inputRef.current.focus()
  }

  return (
    <div>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦</button>
    </div>
  )
}
在 React 19 之前,需要使用 forwardRef 來轉發 ref。現在可以直接將 ref 作為 prop 傳遞。

Ref Callback(回調 Ref)

除了傳遞 ref 物件,你也可以傳遞一個函式(回調 ref)。這在需要測量 DOM 元素或動態管理多個 ref 時很有用:

function MeasuredComponent() {
  const [height, setHeight] = useState(0)

  // 回調 ref:當 DOM 元素建立或銷毀時被呼叫
  const measuredRef = (node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }

  return (
    <div ref={measuredRef}>
      <p>這個區塊的高度是: {height}px</p>
    </div>
  )
}

動態列表的 ref

function ItemList({ items }) {
  const itemRefs = useRef(new Map())

  function scrollToItem(id) {
    const node = itemRefs.current.get(id)
    node?.scrollIntoView({ behavior: 'smooth' })
  }

  return (
    <div>
      <nav>
        {items.map((item) => (
          <button key={item.id} onClick={() => scrollToItem(item.id)}>
            跳到 {item.name}
          </button>
        ))}
      </nav>
      <ul>
        {items.map((item) => (
          <li
            key={item.id}
            ref={(node) => {
              if (node) {
                itemRefs.current.set(item.id, node)
              } else {
                itemRefs.current.delete(item.id)
              }
            }}
          >
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  )
}

注意事項

不要在渲染期間讀寫 ref

// ❌ 錯誤:在渲染期間讀取 ref
function BadComponent() {
  const ref = useRef(0)
  ref.current += 1 // 這可能導致不可預期的行為
  return <p>{ref.current}</p>
}

// ✅ 正確:在事件處理或 useEffect 中讀寫 ref
function GoodComponent() {
  const ref = useRef(0)

  useEffect(() => {
    ref.current += 1
  })

  return <p>元件已渲染</p>
}

ref.current 改變不會通知你

修改 ref.current 不會觸發重新渲染,如果你需要監聽 DOM 變化,考慮使用 ResizeObserverMutationObserver