404 Not Found

404 Not Found


nginx

useRef と DOM の操作

useRef これはまるで金庫のようなものです。中に何でも収納でき、取り出してもその状態は変わりません。useStateとは異なり、ref値を変更しても、コンポーネントの再レンダリングはトリガーされません


1. 学習内容



2. 検索バーの物語

useRefの2つの主な用途

100%
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 useRefuseState の代わりになりますか?
A いいえ。useRef を変更してもコンポーネントの再レンダリングは発生しませんが、setState を変更した場合は再レンダリングが発生します。値を UI に表示する必要がある場合や、レンダリング結果に影響を与える必要がある場合は、useState を使用する必要があります。単に「ユーザーに表示する必要のないデータ」(インターバル ID、前回のレンダリング時の値、WebSocket インスタンスなど)を保存する場合は、useRef を使用してください。useRef は、「再レンダリングをトリガーしない変数コンテナ」と考えてください。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):マウントされた際に自動的にフォーカスを取得する AutoFocusInput コンポーネントを作成し、親コンポーネントから呼び出せる 2 つのメソッド、focus()clear() を実装してください。
  2. 上級演習(難易度 ⭐⭐)ClickCounter コンポーネントを作成し、useRef を使用してクリック数を追跡し(UI には表示しない)、useEffect を使用して、レンダリングのたびにコンソールに「X 回クリックされました」とログを出力してください。
  3. 課題(難易度:⭐⭐⭐)InfiniteScroll コンポーネントを作成し、useRef を使用して、センチネル要素(ページ最下部の特定の要素)がビューポートに入ったことを検知し、それによってさらなるデータの読み込みをトリガーするようにします。IntersectionObserver API を使用してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%