React useRef
useRef 是一個可以讓你保存不需要觸發重新渲染的值的 Hook。它有兩個主要用途:
- 存取 DOM 元素:取得實際的 DOM 元素參照
- 保存可變值:保存不會觸發重新渲染的值
基本語法
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>
)
}
流程說明:
- 使用
useRef(null)建立一個 ref - 將 ref 傳遞給元素的
ref屬性 - React 會將 DOM 元素存入
ref.current - 之後就可以透過
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
| 特性 | useRef | useState |
|---|---|---|
| 改變值會觸發重新渲染 | ❌ 不會 | ✅ 會 |
| 值在渲染間保持 | ✅ 是 | ✅ 是 |
| 可以直接修改 | ✅ 是 | ❌ 否(要用 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 變化,考慮使用 ResizeObserver 或 MutationObserver。