11.1 什麼是 useRef?
useRef 回傳一個可變的 ref 物件,其 .current 屬性在整個元件生命週期中保持不變。
useRef 的兩個主要用途
- 存取 DOM 元素(類似
document.getElementById) - 保存可變值(不會觸發重新渲染)
import { useRef } from 'react';
function BasicRef() {
// 建立 ref
const myRef = useRef(initialValue);
// myRef = { current: initialValue }
// 讀取值
console.log(myRef.current);
// 修改值(不會觸發重新渲染!)
myRef.current = newValue;
}
useRef vs useState
| 比較 | useState | useRef |
|---|---|---|
| 觸發渲染 | ✅ setter 會觸發 | ❌ 不會觸發 |
| 值保存 | ✅ 渲染之間保存 | ✅ 渲染之間保存 |
| 適用場景 | UI 需要反映的資料 | 不需要顯示在 UI 的資料 |
11.2 用 useRef 存取 DOM
基本 DOM 操作
import { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // 聚焦
inputRef.current.select(); // 選取文字
};
const handleScrollToTop = () => {
inputRef.current.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<input ref={inputRef} defaultValue="Hello, React!" />
<button onClick={handleClick}>聚焦 & 選取</button>
</div>
);
}
影片播放器控制
import { useRef, useState } from 'react';
function VideoPlayer() {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
};
return (
<div>
<video
ref={videoRef}
src="https://www.w3schools.com/html/mov_bbb.mp4"
width="400"
/>
<div>
<button onClick={togglePlay}>
{isPlaying ? '⏸ 暫停' : '▶️ 播放'}
</button>
<button onClick={() => { videoRef.current.currentTime = 0; }}>
⏮ 回到開頭
</button>
</div>
</div>
);
}
測量 DOM 元素尺寸
import { useRef, useState, useEffect } from 'react';
function MeasureElement() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width: Math.round(width), height: Math.round(height) });
}
}, []);
return (
<div>
<div
ref={boxRef}
style={{
padding: '20px',
backgroundColor: '#e2e8f0',
borderRadius: '8px',
}}
>
<p>這個元素的尺寸是:</p>
<p>{dimensions.width} × {dimensions.height} px</p>
</div>
</div>
);
}
11.3 用 useRef 保存可變值
保存前一個值
import { useState, useRef, useEffect } from 'react';
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>目前值:{count}</p>
<p>前一個值:{prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
追蹤渲染次數
import { useState, useRef, useEffect } from 'react';
function RenderCounter() {
const [name, setName] = useState('');
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>渲染次數:{renderCount.current}</p>
</div>
);
}
保存 Interval / Timeout ID
import { useState, useRef } from 'react';
function StopWatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
const start = () => {
if (isRunning) return;
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime((prev) => prev + 10);
}, 10);
};
const stop = () => {
clearInterval(intervalRef.current);
setIsRunning(false);
};
const reset = () => {
clearInterval(intervalRef.current);
setIsRunning(false);
setTime(0);
};
const formatTime = (ms) => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const centiseconds = Math.floor((ms % 1000) / 10);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
};
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<h1 style={{ fontSize: '4rem', fontFamily: 'monospace' }}>
{formatTime(time)}
</h1>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<button onClick={start} disabled={isRunning}>開始</button>
<button onClick={stop} disabled={!isRunning}>暫停</button>
<button onClick={reset}>重置</button>
</div>
</div>
);
}
11.4 forwardRef — 將 ref 轉發給子元件
import { useRef, forwardRef } from 'react';
// 子元件用 forwardRef 接收 ref
const FancyInput = forwardRef(({ label, ...props }, ref) => {
return (
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>
{label}
</label>
<input
ref={ref}
{...props}
style={{
padding: '10px 14px',
border: '2px solid #e2e8f0',
borderRadius: '8px',
fontSize: '1rem',
width: '100%',
boxSizing: 'border-box',
}}
/>
</div>
);
});
FancyInput.displayName = 'FancyInput';
// 父元件
function ParentComponent() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = () => {
if (!nameRef.current.value) {
nameRef.current.focus();
alert('請輸入姓名');
return;
}
if (!emailRef.current.value) {
emailRef.current.focus();
alert('請輸入 Email');
return;
}
alert(`姓名:${nameRef.current.value},Email:${emailRef.current.value}`);
};
return (
<div style={{ maxWidth: '400px', margin: '20px auto' }}>
<FancyInput ref={nameRef} label="姓名" placeholder="請輸入姓名" />
<FancyInput ref={emailRef} label="Email" type="email" placeholder="請輸入 Email" />
<button onClick={handleSubmit}>送出</button>
</div>
);
}
11.5 useImperativeHandle — 自訂暴露給父元件的操作
import { useRef, forwardRef, useImperativeHandle, useState } from 'react';
const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
increment: () => setCount((c) => c + 1),
decrement: () => setCount((c) => c - 1),
reset: () => setCount(0),
getValue: () => count,
}));
return (
<div style={{
padding: '16px',
border: '2px solid #e2e8f0',
borderRadius: '8px',
textAlign: 'center',
}}>
<h2>計數器:{count}</h2>
<p style={{ color: '#999' }}>(由父元件控制)</p>
</div>
);
});
Counter.displayName = 'Counter';
function ParentController() {
const counterRef = useRef(null);
return (
<div>
<Counter ref={counterRef} />
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
<button onClick={() => counterRef.current.increment()}>+1</button>
<button onClick={() => counterRef.current.decrement()}>-1</button>
<button onClick={() => counterRef.current.reset()}>重置</button>
<button onClick={() => alert(counterRef.current.getValue())}>取得值</button>
</div>
</div>
);
}
11.6 練習題
練習 1:建立一個自動聚焦的搜尋框
頁面載入時自動聚焦,按 Escape 清空內容並重新聚焦。
練習 2:建立一個無限滾動列表
使用 ref 來觀察底部元素是否進入視窗(Intersection Observer),自動載入更多資料。
本課重點回顧
useRef建立不會觸發渲染的可變容器- 用
ref屬性可以存取 DOM 元素 - 保存 timer ID、前一個值等跨渲染的可變資料
forwardRef讓子元件接收父元件的 refuseImperativeHandle自訂暴露給父元件的方法