React: useRef and DOM Manipulation

Last updated: 2026-10-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:

▶ Try it Yourself

Bob wants to use useState to implement:

▶ Try it Yourself

(2) A Solution Using useRef

▶ Try it Yourself

Output:

TEXT
Buttons: Select All Text, Scroll to the bottom, Measure the height. Input: Autofocus input field. side effects via useEffect. DOM ref via useRef

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

▶ Try it Yourself

Output:

TEXT
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.

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

Output:

TEXT
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

▶ Example: Using useRef to save the previous value

▶ Try it Yourself

Output:

TEXT
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:

▶ 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:

▶ Try it Yourself

7. Complete Example: Rich Text Editor

▶ Expand runnable example (107 lines)
▶ Try it Yourself

Output:

TEXT
State: count (setter: setCount). Button: setCount(c => c + 1)}>+1. useEffect manages side effects. useRef references DOM/variable

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

▶ Try it Yourself

Output:

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

Output:

TEXT
Throttled button: rapid clicks ignored (1 per second max). Click counter + "Throttled!" feedback. useRef stores lastClick timestamp

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

▶ Try it Yourself

Output:

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

Output:

TEXT
Displays: "Click Me". State: clicks (setter: setClicks), feedback (setter: setFeedback). Button: Click Me. useRef references DOM/variable. Timer-based behavior

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

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
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%

🙏 帮我们做得更好

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

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