React: useRef and DOM Manipulation
Last updated: 2026-10-26
useRefIt’s like a safe—you can store anything inside, and it will still be there when you take it out. UnlikeuseState, changingref’s value will not trigger the component to re-render.
1. What You'll Learn
- Basic Usage of useRef (DOM References)
- useRef stores mutable values (without triggering a re-render)
- forwardRef: Passes a ref to a child component
- useImperativeHandle exposes child component methods
- Choosing Between useRef and useState
2. The Story of a Search Bar
The Two Main Uses of useRef
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 YourselfBob wants to use useState to implement:
(2) A Solution Using useRef
▶ Try it Yourself
Output:
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 YourselfOutput:
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 Yourself4. 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 |
Output:
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:
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:
6. useImperativeHandle: Expose a specific method
If you don't want to expose the entire DOM element, use useImperativeHandle to expose only specific methods:
7. Complete Example: Rich Text Editor
▶ Expand runnable example (107 lines)
Output:
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:
"Menu ▾" button → dropdown (Profile, Settings, Logout). Click outside → closes automatically via useClickOutside hook.
Output:
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:
Click → "Clicked!" + count. Click again within 1s → "Too fast! Wait 1 second." (red). Throttled via useRef timestamp.
Output:
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
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>
)
}
Output:
Sign up form: labeled Email & Password inputs with error styling. Submit focuses email. Exposes focus(), selectAll(), scrollIntoView().
❓ FAQ
useRef replace useState?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
useRefreturns to{ current: initialValue }; changing.currentdoes not trigger a re-render- Two uses: ① DOM references (focus, size, scroll) ② Storing variable values (timer ID, previous value)
forwardRef: Allowing a parent component to control a child component's DOM via a refuseImperativeHandle: A method for limiting exposure by providing only the APIs needed by the parent component- Always check for null before operating on ref:
ref.current?.focus()
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create a
AutoFocusInputcomponent that automatically receives focus when mounted, and provide two methods—focus()andclear()—for the parent component to call. - Advanced Exercise (Difficulty ⭐⭐): Create a
ClickCountercomponent, useuseRefto track the number of clicks (without displaying it in the UI), and useuseEffectto log "Clicked X times" to the console on every render. - Challenge (Difficulty: ⭐⭐⭐): Create a
InfiniteScrollcomponent that usesuseRefto listen for the sentinel element (a specific element at the bottom of the page) entering the viewport, triggering the loading of more data. Use theIntersectionObserverAPI.