React: useRef and DOM Manipulation

Last updated: 2026-08-26

useRef It’s like a safe—you can store anything inside, and it will still be there when you take it out. Unlike useState, changing ref’s value will not trigger the component to re-render.


1. What You'll Learn



2. The Story of a Search Bar

The Two Main Uses of useRef

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) Pain Point: Focusing on an input field requires manipulating the DOM

Bob created a search page and wanted the page to automatically focus on the search box when it loads:

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>
  )
}
▶ Try it Yourself

Bob wants to use useState to implement:

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
}
▶ Try it Yourself

(2) A Solution Using 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..." />
}
▶ Try it Yourself

Benefits: useRef returns a mutable object { current: null }, and .current is a real DOM element. No selectors or getElementById are needed—it’s naturally React-friendly.



3. Two Core Uses of useRef

Purpose ref.current points to Triggers a render update Typical scenarios
DOM Reference DOM Element Focus, Scrolling, Measuring Dimensions
Variable Storage Any JavaScript value Timer ID, previous value, non-rendering state

(1) Use Case 1: DOM References

The most common uses—retrieving DOM elements, manipulating focus, size, scroll position, and so on.

Operation Code Description
Focus inputRef.current.focus() Autofocus Input Field
Select Text inputRef.current.select() Select All in Input Field
Scroll to divRef.current.scrollIntoView() Scroll to a specific element
Read Dimensions divRef.current.offsetHeight Get Element Height
Play Video videoRef.current.play() Control Video Playback

▶ Example: 5 Common DOM Operations

Output:

TEXT 📖 Display only
Buttons: Select All Text, Scroll to the bottom, Measure the height. Input: Autofocus input field. side effects via useEffect. DOM ref via useRef
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>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Autofocus input, "Select All Text" button, scrollable 20-row list with "Scroll to bottom", box with "Measure the height" → shows px value

(2) Use Case 2: Storing variable values (without triggering a redraw)

This is useRef’s hidden trick—it stores any value, and the component won’t re-render when that value changes.

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)
▶ Try it Yourself

4. useRef vs. useState

Dimension useRef useState
Return value { current: initial } [value, setter]
Modification Method ref.current = newValue setState(newValue)
Render after modification ❌ Does not trigger ✅ Triggers a re-render
When it is read Reads the latest value immediately Does not read it until the next render
Use Cases DOM operations, timer IDs, previous values Data to be displayed in the 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>
  )
}
▶ Try it Yourself

▶ Example: Using useRef to save the previous value

Output:

TEXT 📖 Display only
Displays: "DOM Example of Operation". Buttons: Select All Text, Scroll to the bottom, Measure the height. Input: Autofocus input field. useEffect manages side effects. useRef references DOM/variable
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
▶ Try it Yourself

Output:

TEXT 📖 Display only
Currently: 0, Last time: (first time). Click +1 → Currently: 1, Last time: 0. Each click shows current and previous value.


5. forwardRef: Parent Components Control the DOM of Child Components

By default, function components do not expose their own DOM references. Using forwardRef allows child components to receive the parent component's 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>
  )
}
▶ Try it Yourself

6. useImperativeHandle: Expose a specific method

If you don't want to expose the entire DOM element, use useImperativeHandle to expose only specific methods:

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>
  )
}
▶ Try it Yourself

7. Complete Example: Rich Text Editor

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'
})

Expected Output: A rich-text editor that supports bold, italics, underline, lists, H2 headings, and other formatting options; content can be saved, loaded, or cleared externally via a ref.


▶ Example 3: Using useRef to Close a Pop-up Window When Clicked from Outside

Output:

TEXT 📖 Display only
State: count (setter: setCount). Button: setCount(c => c + 1)}>+1. useEffect manages side effects. useRef references DOM/variable
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>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
"Menu ▾" button → dropdown (Profile, Settings, Logout). Click outside → closes automatically via useClickOutside hook.

▶ Example 4: Using useRef to Store a Timer for Implementing a Throttled Button

Output:

TEXT 📖 Display only
Throttled button: rapid clicks ignored (1 per second max). Click counter + "Throttled!" feedback. useRef stores lastClick timestamp
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>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Click → "Clicked!" + count. Click again within 1s → "Too fast! Wait 1 second." (red). Throttled via useRef timestamp.

▶ Example 5: Using forwardRef to wrap a focusable Input component

Output:

TEXT 📖 Display only
Displays: "Click Me". State: clicks (setter: setClicks), feedback (setter: setFeedback). Button: Click Me. useRef references DOM/variable. Timer-based behavior
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>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Sign up form: labeled Email & Password inputs with error styling. Submit focuses email. Exposes focus(), selectAll(), scrollIntoView().

❓ FAQ

Q Does forwardRef have to be used on every component?
A No. Use it only in the following scenarios: ① When a parent component needs to directly manipulate a child component’s DOM (e.g., focusing an input field or controlling media playback); ② When encapsulating reusable form components (so the parent component can control focus or selection). If you’re just passing data, using props is sufficient.
Q Can useRef replace useState?
A No. Changes to useRef do not trigger a component re-render, whereas setState does. If your value needs to be displayed in the UI or affect the rendering result, you must use useState. If you’re simply storing “data that doesn’t need to be shown to the user” (such as an interval ID, the value from the previous render, or a WebSocket instance), use useRef. Think of useRef as a “variable container that doesn’t trigger a re-render.”

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Create a AutoFocusInput component that automatically receives focus when mounted, and provide two methods—focus() and clear()—for the parent component to call.
  2. Advanced Exercise (Difficulty ⭐⭐): Create a ClickCounter component, use useRef to track the number of clicks (without displaying it in the UI), and use useEffect to log "Clicked X times" to the console on every render.
  3. Challenge (Difficulty: ⭐⭐⭐): Create a InfiniteScroll component that uses useRef to listen for the sentinel element (a specific element at the bottom of the page) entering the viewport, triggering the loading of more data. Use the IntersectionObserver API.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏