Master useRef for DOM access, mutable values, and breaking stale closures — without triggering re-renders.
Think of useRef as a sticky note attached to your component. You can write anything on it, erase it, and rewrite it — and the component never notices. No re-render happens. The sticky note persists for the entire lifetime of the component, through every render, silently holding its value. This is the fundamental difference from useState: changing a ref's value is invisible to React. The UI does not update. The component does not re-render. The ref just quietly holds whatever you put in it. useRef has two completely different use cases that share the same mechanism. The first is DOM access — you attach the ref to a JSX element, and React automatically writes the actual DOM node into ref.current when the element mounts. The second is storing any mutable value that needs to survive renders without causing them: timer IDs, previous values, instance variables, WebSocket connections, flags. The key question to ask before using useRef: "Should the UI update when this value changes?" If yes — use state. If no — use a ref.
useRef(initialValue) returns a plain object: { current: initialValue }. This object is created once when the component mounts and the same object is returned on every subsequent render. Mutating .current does not trigger a re-render — React never reads ref.current during its render cycle.
const ref = useRef(0)
// ref is: { current: 0 }
ref.current = 42 // mutation — React does NOT know, does NOT re-render
ref.current // 42 — on the next render, still 42
Pass a ref to a JSX element's ref attribute. React sets ref.current to the DOM node when the element mounts, and sets it back to null when it unmounts.
function TextInput() {
const inputRef = useRef(null)
function focusInput() {
inputRef.current.focus() // direct DOM access
inputRef.current.select() // select all text
inputRef.current.scrollIntoView() // scroll to element
}
return (
<>
<input ref={inputRef} type="text" placeholder="Type here..." />
<button onClick={focusInput}>Focus</button>
</>
)
}
Important: ref.current is null during render. The DOM node is only available after mount, so only access it inside useEffect, event handlers, or useLayoutEffect — never in the render body.
function Component() {
const ref = useRef(null)
// ✗ Wrong — ref.current is null during render
console.log(ref.current.offsetWidth)
// ✓ Correct — DOM is available after mount
useEffect(() => {
console.log(ref.current.offsetWidth) // works ✓
}, [])
return <div ref={ref}>Hello</div>
}
Store any value that needs to persist across renders but shouldn't trigger a re-render when it changes:
// Timer ID — changing it shouldn't re-render the component
function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef(null)
function start() {
if (intervalRef.current) return // already running
intervalRef.current = setInterval(() => setCount(c => c + 1), 1000)
}
function stop() {
clearInterval(intervalRef.current)
intervalRef.current = null
}
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
One of the most powerful patterns: store the latest version of a value or callback in a ref so that closures always read the current value, even without listing it as a dependency.
// Problem: stale closure in setInterval
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // stale — always 0
}, 1000)
return () => clearInterval(id)
}, [])
// Fix: store latest count in ref
const countRef = useRef(count)
useEffect(() => { countRef.current = count }, [count])
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current) // always current ✓
}, 1000)
return () => clearInterval(id)
}, []) // safe — countRef is stable
}
Always-current callback pattern:
// Stable function that always calls the latest version of a callback
function useLatestCallback(fn) {
const fnRef = useRef(fn)
useEffect(() => { fnRef.current = fn }) // update after every render
return useCallback((...args) => fnRef.current(...args), [])
}
function usePrevious(value) {
const ref = useRef(undefined)
useEffect(() => { ref.current = value }) // runs AFTER render
return ref.current // during current render = previous render's value
}
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return <p>Now: {count} | Before: {prevCount}</p>
}
By default, you cannot attach a ref to a custom functional component. React.forwardRef lets a parent pass a ref through to a DOM element inside the child.
// Without forwardRef — ref.current is null
function Input(props) { return <input {...props} /> }
<Input ref={ref} /> // ref.current = null!
// With forwardRef — ref reaches the input
const Input = React.forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />
})
function Form() {
const ref = useRef(null)
return (
<>
<Input ref={ref} placeholder="Name" />
<button onClick={() => ref.current.focus()}>Focus</button>
</>
)
}
const Input = React.forwardRef((props, ref) => {
const innerRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => innerRef.current.focus(),
clear: () => { innerRef.current.value = '' },
getValue: () => innerRef.current.value,
// Parent can ONLY call these — cannot access the raw DOM node
}))
return <input ref={innerRef} {...props} />
})
Instead of a ref object, pass a function. React calls it with the DOM node on mount and null on unmount.
function MeasuredDiv() {
const [height, setHeight] = useState(0)
const callbackRef = useCallback(node => {
if (node !== null) setHeight(node.offsetHeight)
}, [])
return <div ref={callbackRef}>Content — height: {height}px</div>
}
// Conditional render — object ref misses the attach moment
// Callback ref fires immediately when element appears
function ConditionalMap({ show }) {
const mapRef = useCallback(node => {
if (node) initMapLibrary(node)
}, [])
return show ? <div ref={mapRef} id="map" /> : null
}
| Scenario | Use | Why |
|---|---|---|
| Counter to display to user | useState | UI must update |
| Interval/timer ID | useRef | No UI update needed |
| DOM node access | useRef | Imperative DOM API |
| Previous value tracking | useRef | Silent, no re-render |
| Form input value (controlled) | useState | Value drives UI |
| Form input value (submit only) | useRef | Read on demand |
| WebSocket instance | useRef | Instance variable |
| isMounted flag | useRef | Silent flag |
Many developers think mutating ref.current triggers a re-render — but React never reads ref.current during rendering. The mutation is completely invisible to React. If you update a ref and expect the UI to change, nothing happens. If you need the UI to update, use state.
Many developers think useRef is only for DOM access — but storing mutable instance variables is equally important. Timer IDs, WebSocket connections, previous values, mounted flags, and latest-callback patterns all use refs for non-DOM purposes.
Many developers think you can read ref.current in the render body to access a DOM node — but during render, the DOM hasn't been created yet (or has been unmounted). ref.current is null during render. Access DOM refs only inside useEffect, useLayoutEffect, or event handlers.
Many developers think forwardRef is needed to pass any ref to a child — but forwardRef is only necessary when you want a parent's ref to attach to a DOM element inside a child functional component. For class components, refs attach to the instance automatically. React 19 removes the need for forwardRef entirely — refs become plain props.
Many developers think useRef and createRef are interchangeable — but createRef creates a new ref object on every render. In class components, createRef is used in the constructor (once). In functional components, always use useRef — it returns the same object every render.
Many developers think changing ref.current inside useEffect is equivalent to setting state — but the component won't re-render after the effect runs if you only update a ref. If you need to re-render after an async operation, call a state setter.
React Hook Form uses refs internally to track form field values without triggering re-renders on every keystroke — this is how it achieves better performance than controlled forms with useState. Every input is registered via a callback ref and values are read imperatively on submit, not continuously tracked in state.
Video and audio players built in React store the media element ref and call imperative methods: play(), pause(), seek(), volume adjustment. These DOM APIs don't exist in React's declarative model — they must be called imperatively on the DOM element through a ref.
Rich text editors like Draft.js and Tiptap use refs to manage editor instances. The editor is initialized in useEffect on the DOM ref, and commands (bold, italic, insert) are called imperatively on the editor instance stored in a separate ref.
Infinite scroll implementations store the intersection observer in a ref and attach it via a callback ref to the last list item. When the item enters the viewport, fetchNextPage() is called. The observer ref is cleaned up in the cleanup function.
Focus management in accessible UIs — modals, drawers, comboboxes — uses refs to move focus to the first interactive element on open and return focus to the trigger element on close. These focus patterns are impossible with declarative React alone.
Animation libraries like GSAP and Framer Motion (imperative API) use refs to target DOM elements directly. React cannot animate DOM properties declaratively at the frame level — the animation library needs the raw DOM node to apply transforms.
No questions tagged to this topic yet.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.