useRef と DOM の操作
useRefこれはまるで金庫のようなものです。中に何でも収納でき、取り出してもその状態は変わりません。useStateとは異なり、refの値を変更しても、コンポーネントの再レンダリングはトリガーされません。
1. 学習内容
- useRef の基本的な使い方(DOM リファレンス)
- useRef は(再レンダリングをトリガーすることなく)変更可能な値を格納します
- forwardRef: 子コンポーネントへの参照を渡す
- useImperativeHandle は子コンポーネントのメソッドを公開します
- useRef と useState のどちらを選ぶか
2. 検索バーの物語
useRefの2つの主な用途
flowchart LR
subgraph "Uses1: DOMQuote"
A[useRef] --> B["ref={inputRef}"]
B --> C["inputRef.current.focus()"]
end
subgraph "Uses2: Variable-Value Storage"
D[useRef] --> E[".current = value]
E --> F[Does not trigger a repaint]
end
G[forwardRef] --> H[Passed from the parent componentref]
H --> I[Child Component ExposureDOM]
style A fill:#e3f2fd,stroke:#1565c0
style D fill:#e8f5e9,stroke:#2e7d32
style G fill:#fff3e0,stroke:#e65100
(1) 課題:入力フィールドにフォーカスを当てるには、DOMを操作する必要がある
ボブは検索ページを作成し、ページが読み込まれた際に検索ボックスに自動的にフォーカスが当たるようにしたかった:
JSX
function SearchPage() {
// ❌ Question:There is no direct way to focus on the input field.
return (
<div>
<input type="text" placeholder="Search..." />
{/* How can I make this input field automatically receive focus?? */}
{/* Cannot be used document.getElementById,React Direct manipulation is not recommended. DOM */}
</div>
)
}
ボブはuseStateを使って、以下を実装したいと考えています:
JSX
// ❌ Error:useState Updates trigger a re-render,But I don't know how to use it DOM
function SearchPage() {
const [input, setInput] = React.useState(null)
React.useEffect(() => {
// Hope input is DOM Element...
// But how do you DOM Element Storage state in ?
}, [])
return <input ref={el => setInput(el)} /> // Function ref,The grammar is strange
}
(2) useRef を用いた解決策
JSX
import { useRef, useEffect } from 'react'
function SearchPage() {
const inputRef = useRef(null) // Create ref
useEffect(() => {
// ✅ Automatically focus on the component after it is mounted
inputRef.current.focus()
}, [])
return <input ref={inputRef} type="text" placeholder="Autofocus search box..." />
}
利点:useRef は可変オブジェクト { current: null } を返し、.current は実際の DOM 要素です。セレクタや getElementById は不要で、React と自然に連携します。
3. useRefの2つの主な用途
| 目的 | ref.current が指す先 | レンダリングの更新をトリガーする | 代表的なシナリオ |
|---|---|---|---|
| DOM リファレンス | DOM 要素 | ❌ | フォーカス、スクロール、寸法の測定 |
| 変数の格納 | 任意の JavaScript 値 | ❌ | タイマー ID、前回の値、レンダリングされていない状態 |
(1) ユースケース 1: DOM 参照
最も一般的な用途は、DOM要素の取得、フォーカスやサイズ、スクロール位置の操作などです。
| 操作 | コード | 説明 |
|---|---|---|
| フォーカス | inputRef.current.focus() |
オートフォーカス入力欄 |
| テキストを選択 | inputRef.current.select() |
入力フィールド内のすべてを選択 |
| スクロール先 | divRef.current.scrollIntoView() |
特定の要素までスクロール |
| 寸法を読み取る | divRef.current.offsetHeight |
要素の高さを取得 |
| 動画を再生 | videoRef.current.play() |
動画再生の操作 |
(1) ▶ サンプル:つの一般的なDOM操作
JSX
// ============================================
// Example:useRef 's 5 Common Types DOM Operation
// ============================================
function DomOperations() {
const inputRef = useRef(null)
const videoRef = useRef(null)
const listRef = useRef(null)
const boxRef = useRef(null)
const [boxHeight, setBoxHeight] = React.useState(0)
// 1. Autofocus
useEffect(() => { inputRef.current.focus() }, [])
function handleSelect() { inputRef.current.select() } // 2. Select All
function handlePlay() { videoRef.current.play() } // 3. Play
function handlePause() { videoRef.current.pause() } // 3. Pause
function handleScroll() { listRef.current.scrollIntoView({ behavior: 'smooth' }) } // 4. Scroll
function handleMeasure() { // 5. Measure Dimensions
setBoxHeight(boxRef.current.offsetHeight)
}
return (
<div>
<h3>DOM Example of Operation</h3>
<input ref={inputRef} placeholder="Autofocus input field" />
<button onClick={handleSelect}>Select All Text</button>
<div ref={listRef} style={{ height: '100px', overflow: 'auto', border: '1px solid #ddd', margin: '10px 0' }}>
{Array.from({ length: 20 }, (_, i) => <p key={i}>Row {i + 1}</p>)}
</div>
<button onClick={handleScroll}>Scroll to the bottom</button>
<div ref={boxRef} style={{ padding: '20px', backgroundColor: '#f0f0f0', margin: '10px 0' }}>
<p>The height of this element is:{boxHeight}px</p>
<button onClick={handleMeasure}>Measure the height</button>
</div>
</div>
)
}
(2) ユースケース 2:変数の値を保存する(再描画をトリガーせずに)
これがuseRefの隠し技です。どんな値でも格納でき、その値が変わってもコンポーネントは再レンダリングされません。
JSX
function Stopwatch() {
const [time, setTime] = React.useState(0)
const timerRef = useRef(null) // Storage Timer ID,Do not participate in the rendering
function start() {
if (timerRef.current) return // Prevent Repeated Starts
timerRef.current = setInterval(() => {
setTime(t => t + 1)
}, 1000)
}
function stop() {
clearInterval(timerRef.current)
timerRef.current = null // Reset
}
return (
<div>
<p>Timer: {time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
// timerRef Storage Timer ID
// Change timerRef.current Will not trigger a re-render
// The timer is not automatically cleared when the component is unloaded.(I remember back in useEffect Clean up)
4. useRef 対 useState
| ディメンション | useRef | useState |
|---|---|---|
| 戻り値 | { current: initial } |
[value, setter] |
| 変更方法 | ref.current = newValue |
setState(newValue) |
| 変更後のレンダリング | ❌ トリガーされない | ✅ 再レンダリングがトリガーされる |
| 読み込まれるタイミング | 最新の値を即座に読み込む | 次のレンダリングが行われるまで読み込まない |
| ユースケース | DOM操作、タイマーID、以前の値 | UIに表示されるデータ |
JSX
function RefVsState() {
const renderCount = useRef(1) // A counter that won't be triggered
const [count, setCount] = useState(0) // Counters that will be triggered
useEffect(() => {
renderCount.current += 1 // Change it however you like,Does not trigger a repaint
})
return (
<div>
<p>useState:{count}(Click to trigger rendering)</p>
<p>useRef:{renderCount.current}(View only in the console)</p>
<button onClick={() => setCount(c => c + 1)}>useState +1</button>
<button onClick={() => renderCount.current += 1}>useRef +1(No change)</button>
</div>
)
}
(1) ▶ サンプル:useRef を使用して前の値を保存する
JSX
// ============================================
// Example:Go back to props/state value
// ============================================
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value // Update after each render
})
return ref.current // Go back to the previous value
}
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count) // The previous value
return (
<div>
<p>Currently:{count}</p>
<p>Last time:{prevCount !== undefined ? prevCount : '(For the first time)'}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
// Process:
// 1. Initial Rendering:count=0, prevCount=undefined
// 2. Click +1 → count=1
// 3. useEffect Update prev=0 → Back 0
// 4. Click again +1 → count=2, prev=1
5. forwardRef:親コンポーネントによる子コンポーネントのDOMの制御
デフォルトでは、関数コンポーネントは自身のDOM参照を公開しません。forwardRef を使用することで、子コンポーネントは親コンポーネントの ref を受け取ることができます:
JSX
// ---- Child component:use forwardRef Package ----
const CustomInput = forwardRef(function CustomInput(props, ref) {
return (
<div style={{ border: '1px solid #d9d9d9', padding: '4px', borderRadius: '4px' }}>
<input ref={ref} {...props} style={{ border: 'none', outline: 'none', width: '100%' }} />
</div>
)
})
// ---- Parent Component:Use directly ref Controlling subcomponents input ----
function Form() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus() // ✅ Can focus directly on CustomInput Internal input
}, [])
return (
<div>
<CustomInput ref={inputRef} placeholder="Input fields controlled by the parent component" />
<button onClick={() => inputRef.current.focus()}>In Focus</button>
<button onClick={() => inputRef.current.select()}>Select All</button>
</div>
)
}
6. useImperativeHandle: 特定のメソッドを公開する
DOM要素全体を公開したくない場合は、useImperativeHandle を使用して特定のメソッドのみを公開してください:
JSX
// ============================================
// Example:Custom Player——Expose only play/pause,Without revealing the whole video
// ============================================
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null)
// Expose only 3 A method for the parent component
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play()
},
pause() {
videoRef.current.pause()
},
jumpTo(seconds) {
videoRef.current.currentTime = seconds
}
}))
return <video ref={videoRef} src={src} controls style={{ width: '100%' }} />
})
// ---- Usage -----
function App() {
const playerRef = useRef(null)
return (
<div>
<h3>Video Player</h3>
<VideoPlayer ref={playerRef} src="https://example.com/video.mp4" />
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
<button onClick={() => playerRef.current.play()}>▶ Play</button>
<button onClick={() => playerRef.current.pause()}>⏸ Pause</button>
<button onClick={() => playerRef.current.jumpTo(30)}>⏭ Jump to 30s</button>
</div>
{/* The parent component cannot be manipulated directly video DOM,You can only call the exposed play/pause/jumpTo */}
</div>
)
}
7. 完全な例:リッチテキストエディタ
JSX
// ============================================
// Complete Example:Simple Rich Text Editor
// Features:useRef Control contentEditable Region
// forwardRef + useImperativeHandle Exposure Methods
// ============================================
const RichEditor = forwardRef(function RichEditor({ placeholder }, ref) {
const editorRef = useRef(null)
const [isEmpty, setIsEmpty] = React.useState(true)
// Exposing Methods to the Parent Component
useImperativeHandle(ref, () => ({
getContent() {
return editorRef.current.innerHTML
},
setContent(html) {
editorRef.current.innerHTML = html
checkEmpty()
},
clear() {
editorRef.current.innerHTML = ''
setIsEmpty(true)
editorRef.current.focus()
},
focus() {
editorRef.current.focus()
}
}))
function checkEmpty() {
const text = editorRef.current.textContent || ''
setIsEmpty(text.trim().length === 0)
}
function handleKeyDown(e) {
if (e.ctrlKey && e.key === 'b') {
document.execCommand('bold')
e.preventDefault()
}
if (e.ctrlKey && e.key === 'i') {
document.execCommand('italic')
e.preventDefault()
}
}
return (
<div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', overflow: 'hidden' }}>
{/* Toolbar */}
<div style={{ padding: '8px', borderBottom: '1px solid #d9d9d9', backgroundColor: '#fafafa', display: 'flex', gap: '4px' }}>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('bold') }} style={toolBtnStyle}><b>B</b></button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('italic') }} style={toolBtnStyle}><i>I</i></button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('underline') }} style={toolBtnStyle}><u>U</u></button>
<span style={{ color: '#ddd' }}>|</span>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('insertUnorderedList') }} style={toolBtnStyle}>List</button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('formatBlock', false, 'h2') }} style={toolBtnStyle}>H2</button>
</div>
{/* Edit Area */}
<div
ref={editorRef}
contentEditable
onInput={checkEmpty}
onKeyDown={handleKeyDown}
style={{
minHeight: '200px',
padding: '16px',
outline: 'none',
lineHeight: '1.6'
}}
data-placeholder={placeholder}
{...(isEmpty ? { 'data-empty': 'true' } : {})}
/>
</div>
)
})
function App() {
const editorRef = useRef(null)
const [savedContent, setSavedContent] = React.useState('')
function handleSave() {
const content = editorRef.current.getContent()
setSavedContent(content)
alert('Saved!')
}
function handleClear() {
editorRef.current.clear()
}
function handleLoad() {
editorRef.current.setContent('<h2>Loaded content</h2><p>This is loaded from an external source. HTML。</p>')
}
return (
<div style={{ maxWidth: '700px', margin: '0 auto' }}>
<h2>📝 Rich Text Editor</h2>
<RichEditor ref={editorRef} placeholder="Start Writing..." />
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
<button onClick={handleSave} style={btnStyle('#1890ff')}>💾 Save</button>
<button onClick={handleLoad} style={btnStyle('#52c41a')}>📂 Loading Example</button>
<button onClick={handleClear} style={btnStyle('#ff4d4f')}>🗑 Clear</button>
</div>
{savedContent && (
<div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<p style={{ fontWeight: 'bold', margin: '0 0 8px 0' }}>Saved Content:</p>
<div style={{ fontSize: '13px', color: '#666', wordBreak: 'break-all', fontFamily: 'monospace' }}>
{savedContent}
</div>
</div>
)}
</div>
)
}
const toolBtnStyle = {
padding: '4px 10px', border: '1px solid transparent',
borderRadius: '3px', backgroundColor: 'transparent',
cursor: 'pointer', fontSize: '14px'
}
const btnStyle = (color) => ({
padding: '8px 16px', backgroundColor: color,
color: 'white', border: 'none',
borderRadius: '4px', cursor: 'pointer'
})
期待される出力:太字、斜体、下線、リスト、H2見出し、その他の書式設定オプションをサポートするリッチテキストエディタ。コンテンツは、
refを通じて外部で保存、読み込み、またはクリアすることができます。
(1) ▶ サンプル:useRef を使用して、外部からクリックされた際にポップアップウィンドウを閉じる
JSX
function useClickOutside(callback) {
const ref = useRef(null)
useEffect(() => {
function handler(e) {
if (ref.current && !ref.current.contains(e.target)) {
callback()
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [callback])
return ref
}
function Dropdown() {
const [open, setOpen] = useState(false)
const dropdownRef = useClickOutside(() => setOpen(false))
return (
<div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<button onClick={() => setOpen(!open)} style={{ padding: '8px 16px', cursor: 'pointer' }}>
Menu ▾
</button>
{open && (
<div style={{ position: 'absolute', top: '100%', left: 0, background: 'white', border: '1px solid #ddd', borderRadius: 4, minWidth: 120, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
{['Profile', 'Settings', 'Logout'].map(item => (
<div key={item} onClick={() => { setOpen(false) }}
style={{ padding: '8px 16px', cursor: 'pointer' }}>
{item}
</div>
))}
</div>
)}
</div>
)
}
(2) ▶ サンプル:useRef を使用してタイマーを保存し、スロットリング機能付きボタンを実装する
JSX
function ThrottledButton() {
const lastClickRef = useRef(0)
const [clicks, setClicks] = useState(0)
const [feedback, setFeedback] = useState('')
function handleClick() {
const now = Date.now()
if (now - lastClickRef.current < 1000) {
setFeedback('Too fast! Wait 1 second.')
return
}
lastClickRef.current = now
setClicks(c => c + 1)
setFeedback('Clicked!')
setTimeout(() => setFeedback(''), 500)
}
return (
<div style={{ textAlign: 'center', padding: 20 }}>
<button onClick={handleClick}
style={{ padding: '10px 24px', fontSize: 16, cursor: 'pointer' }}>
Click Me
</button>
<p>Clicks: {clicks}</p>
{feedback && <p style={{ color: feedback.includes('Too') ? '#ff4d4f' : '#52c41a' }}>{feedback}</p>}
</div>
)
}
(3) ▶ サンプル:forwardRef を使用して、フォーカス可能な Input コンポーネントをラップする
JSX
const FancyInput = React.forwardRef(function FancyInput({ label, error, ...props }, ref) {
const internalRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => internalRef.current?.focus(),
selectAll: () => {
const el = internalRef.current
if (el) { el.focus(); el.select() }
},
scrollIntoView: () => internalRef.current?.scrollIntoView({ behavior: 'smooth' }),
}))
return (
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>{label}</label>
<input
ref={internalRef}
style={{ width: '100%', padding: 8, borderRadius: 4, border: `1px solid ${error ? '#ff4d4f' : '#d9d9d9'}`, outline: 'none' }}
{...props}
/>
{error && <p style={{ color: '#ff4d4f', fontSize: 12, margin: '4px 0 0' }}>{error}</p>}
</div>
)
})
function FormWithFancyInput() {
const emailRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
emailRef.current?.focus()
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Sign Up</h3>
<FancyInput ref={emailRef} label="Email" type="email" placeholder="you@example.com" />
<FancyInput label="Password" type="password" placeholder="At least 6 characters" />
<button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
</form>
)
}
❓ よくある質問
Q すべてのコンポーネントで forwardRef を使用する必要がありますか?
A いいえ。以下のケースでのみ使用してください:① 親コンポーネントが子コンポーネントの DOM を直接操作する必要がある場合(例:入力フィールドにフォーカスを当てたり、メディアの再生を制御したりする場合);② 再利用可能なフォームコンポーネントをカプセル化する場合(親コンポーネントがフォーカスや選択を制御できるようにするため)。単にデータを渡すだけなら、props を使用すれば十分です。
Q
useRef は useState の代わりになりますか?A いいえ。
useRef を変更してもコンポーネントの再レンダリングは発生しませんが、setState を変更した場合は再レンダリングが発生します。値を UI に表示する必要がある場合や、レンダリング結果に影響を与える必要がある場合は、useState を使用する必要があります。単に「ユーザーに表示する必要のないデータ」(インターバル ID、前回のレンダリング時の値、WebSocket インスタンスなど)を保存する場合は、useRef を使用してください。useRef は、「再レンダリングをトリガーしない変数コンテナ」と考えてください。📖 まとめ
useRefが{ current: initialValue }に戻る;.currentを変更しても 再レンダリングは発生しない- 2つの用途:① DOM参照(フォーカス、サイズ、スクロール) ② 変数値の保存(タイマーID、前回の値)
forwardRef: 親コンポーネントが ref を通じて子コンポーネントの DOM を制御できるようにするuseImperativeHandle: 親コンポーネントが必要とするAPIのみを提供することで、公開範囲を制限する方法- ref に対して操作を行う前には、必ず null かどうかを確認してください:
ref.current?.focus()
📝 練習問題
- 基本演習(難易度 ⭐):マウントされた際に自動的にフォーカスを取得する
AutoFocusInputコンポーネントを作成し、親コンポーネントから呼び出せる 2 つのメソッド、focus()とclear()を実装してください。 - 上級演習(難易度 ⭐⭐):
ClickCounterコンポーネントを作成し、useRefを使用してクリック数を追跡し(UI には表示しない)、useEffectを使用して、レンダリングのたびにコンソールに「X 回クリックされました」とログを出力してください。 - 課題(難易度:⭐⭐⭐):
InfiniteScrollコンポーネントを作成し、useRefを使用して、センチネル要素(ページ最下部の特定の要素)がビューポートに入ったことを検知し、それによってさらなるデータの読み込みをトリガーするようにします。IntersectionObserverAPI を使用してください。



