useRef returns a mutable object with a single .current property,
initialized to the argument you pass. The object persists for the full lifetime
of the component — across renders — and changing .current does not
trigger a re-render.
const ref = useRef(null)
// ref.current is null until attached to a DOM element or set manually
ref.current = 42 // silent mutation, no re-render
Two primary uses: (1) holding a DOM element reference and (2) storing a mutable instance variable (like a timer ID or a previous value) that must survive renders without causing them.
| useState | useRef | |
|---|---|---|
| Triggers re-render | Yes | No |
| Value persists across renders | Yes | Yes |
| Used to drive the UI | Yes | No |
| Mutable | No (replace via setter) | Yes (mutate .current directly) |
const [count, setCount] = useState(0) // UI re-renders when count changes
const renders = useRef(0) // bookkeeping; changing it is invisible
renders.current++
If the screen needs to show the value, it belongs in state. If you need to remember something between renders without affecting the UI, use a ref.
Pass the ref object to a JSX element's ref attribute. After the component
mounts, ref.current holds the underlying DOM node.
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus() // focus the input after mount
}, [])
return <input ref={inputRef} />
ref.current is null during the first render (the DOM node doesn't exist yet).
It's populated after the component mounts and set back to null on unmount.
Instead of a ref object, you can pass a function to ref. React calls it
with the DOM node when it mounts (and null when it unmounts). This is useful
when you need to know the exact moment the element attaches.
function MeasureDiv() {
const [height, setHeight] = useState(null)
const measuredRef = useCallback(node => {
if (node !== null) setHeight(node.getBoundingClientRect().height)
}, [])
return <div ref={measuredRef}>content</div>
}
A regular ref object doesn't notify you when the node appears or changes — that's the gap callback refs fill. They're also the right approach for conditional elements or lists where the number of nodes changes.
By default, you can't attach a ref to a custom function component — the ref
goes to the component instance, which doesn't exist for function components.
forwardRef lets a parent reach a DOM node inside the child.
const Input = forwardRef(function Input({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
)
})
function Form() {
const inputRef = useRef(null)
return <Input label="Name" ref={inputRef} />
// inputRef.current is the <input> DOM node
}
Use forwardRef for reusable input or UI components where the parent legitimately
needs direct DOM access (focus management, scroll, measurement).
useImperativeHandle customizes what a parent sees when it holds a ref to your
component — instead of a DOM node, you expose a controlled API of your choosing.
const Dialog = forwardRef(function Dialog(props, ref) {
const [open, setOpen] = useState(false)
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
return open ? <div>dialog</div> : null
})
function App() {
const dialogRef = useRef(null)
return (
<>
<button onClick={() => dialogRef.current.open()}>show</button>
<Dialog ref={dialogRef} />
</>
)
}
This is an escape hatch for imperative integration. Prefer declarative props
in most cases; use useImperativeHandle when consumers genuinely need to trigger
behavior imperatively (animations, focus sequences, scroll).
Assign the timer ID to ref.current inside an effect. It persists across renders
without triggering them, and you can read it in the cleanup function.
const timerId = useRef(null)
useEffect(() => {
timerId.current = setInterval(tick, 1000)
return () => clearInterval(timerId.current)
}, [])
function stop() {
clearInterval(timerId.current)
}
Using state for a timer ID would cause a re-render each time you start or stop the interval — unnecessary since the ID never appears in the UI.
Store the previous value in a ref, updating it at the end of each render via an effect (or by manually updating it after using it in the render body).
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value // update after render
})
return ref.current // returns the value from the previous render
}
const prevCount = usePrevious(count)
The effect runs after the render, so ref.current on the current render still
holds the value from the previous render — which is what the caller wants.
Keep a ref mirroring the latest value. The callback reads ref.current instead
of the closed-over snapshot, so it always sees fresh data without being
re-created.
const onTickRef = useRef(onTick)
useEffect(() => { onTickRef.current = onTick }) // sync every render
useEffect(() => {
const id = setInterval(() => onTickRef.current(), 1000)
return () => clearInterval(id)
}, []) // interval created once; always calls the latest onTick
This is the foundation of the useEventCallback pattern used in many hook
libraries to avoid re-creating timers, WebSockets, or subscriptions every time
a callback prop changes.
A module-level let or const is shared across all instances of the
component. A ref is per-instance — each mounted copy of the component gets
its own ref.current.
let moduleTimer = null // shared by ALL Poller instances — bug!
function Poller() {
const timerRef = useRef(null) // each Poller has its own timer
// ...
}
Use refs for per-instance mutable values. Use module variables only for truly shared, singleton state (e.g., a global cache).
ref.current is null before mount and after unmount. React sets it to the DOM
node when the element is attached, then back to null when it's removed.
useEffect(() => {
// safe: effect only runs after mount, ref.current is the DOM node
ref.current.focus()
}, [])
// dangerous: reading ref.current during the first render body
if (ref.current) ref.current.focus() // null on first render — doesn't work
Always access DOM refs inside effects or event handlers, never during render.
Because mutating ref.current doesn't trigger a re-render, the UI won't update
to reflect the new value. If you need to display or react to a value, it must be
state.
const ref = useRef(0)
ref.current++ // counter increments, but nothing on screen changes
const [count, setCount] = useState(0)
setCount(c => c + 1) // screen updates
A common mistake: using a ref to "avoid renders" for a value that's actually displayed in the JSX. The component won't visually update. Refs are for invisible bookkeeping.
Changing a key tells React to unmount the old instance and mount a new one.
The new instance starts with a fresh useRef(null) — the old ref's .current
is set to null and the new instance creates its own ref.
// each time userId changes, the form unmounts and remounts
<UserForm key={userId} />
// any refs inside UserForm are re-created fresh
This is expected and usually desirable — the key reset clears both state and
refs. If a parent holds a ref to the child's DOM node, the parent's ref is also
reset after the remount.
Function components have no instance — there's nothing for ref to point at.
React will silently ignore the prop and ref.current stays null, which often
causes a confusing bug.
const MyInput = ({ value }) => <input value={value} />
const ref = useRef(null)
<MyInput ref={ref} /> // ref.current is null — not the <input>
The fix is to wrap MyInput in forwardRef and pass the ref down to the DOM
element. Since React 19, ref is a plain prop on function components and
forwardRef is no longer needed — but it's still needed in React 18 and below.
Always prefer refs in React. getElementById queries the global document and
breaks if the same component is rendered multiple times or if IDs clash. A ref is
scoped to the specific DOM node of this component instance.
// fragile: relies on a unique ID existing in the document
document.getElementById('my-input').focus()
// correct: directly scoped to this component's node
const inputRef = useRef(null)
inputRef.current?.focus()
return <input ref={inputRef} id="my-input" />
Refs are also automatically cleaned up (set to null) when the element unmounts,
unlike raw DOM queries that can hold stale references.
More Hooks interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.