Skip to content

React · Hooks

React useRef Hook — Complete Interview Guide with Examples

7 min read Updated 2026-06-23 Share:

Practice useRef interview questions

What the React useRef hook does — and why interviewers love it

The React useRef hook is one of the most misunderstood pieces of the hooks API. On the surface it's just a way to grab a DOM node. Beneath that, it's a general-purpose mutable container that survives renders without causing them — and understanding that distinction separates "I know hooks" from "I understand React's rendering model."

useRef comes up in almost every senior React interview because it sits at the intersection of React's declarative model and the imperative browser APIs that lie outside it. Questions range from "how do you focus an input programmatically?" to "what is forwardRef and when would you use it?" to "how do you avoid stale closures in a long-lived effect?" All of them are useRef questions in disguise.

The shape of useRef

useRef(initialValue) returns a plain object with a single property: .current. That object is created once and persists for the lifetime of the component. Crucially, mutating .current does not trigger a re-render.

const ref = useRef(null)
// ref.current === null until something sets it

ref.current = 42  // mutation — no re-render

Compare this with useState: state changes cause renders; ref mutations don't. This is the central difference. If the value needs to be visible on screen, it belongs in state. If it's invisible bookkeeping, a ref is the right tool.

Accessing DOM nodes

The most common useRef use case: grab a DOM node to do something the React model can't express declaratively — focus, scroll, measure.

function SearchBar() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus() // focus after mount
  }, [])

  return <input ref={inputRef} placeholder="Search..." />
}

React sets ref.current to the DOM node when the component mounts and resets it to null when the component unmounts. Read DOM refs inside effects or event handlers — ref.current is null during the first render pass.

useRef vs useState — when to reach for which

useRefuseState
Triggers re-renderNoYes
Persists across rendersYesYes
Use for UI outputNoYes
Mutate directlyYes (ref.current = x)No (use setter)
const [count, setCount] = useState(0) // displayed on screen -> needs state
const renderCount = useRef(0)          // bookkeeping -> ref is enough
renderCount.current++

A common mistake: using a ref for a value that's displayed in JSX. The UI won't update because React never knows the ref changed. If the view depends on the value, it must be state.

Storing mutable values: timers, IDs, and previous values

Any mutable "instance variable" that needs to survive renders without causing them is a useRef candidate.

Timer ID:

const timerId = useRef(null)

useEffect(() => {
  timerId.current = setInterval(tick, 1000)
  return () => clearInterval(timerId.current)
}, [])

function stop() {
  clearInterval(timerId.current)
}

Previous value:

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => { ref.current = value }) // updates after render
  return ref.current                        // returns last render's value
}

The previous-value pattern works because useEffect (with no deps) runs after every render — so ref.current during the current render still holds the value from the previous one.

The "latest value" ref pattern for long-lived effects

A long-lived effect (WebSocket, setInterval, event listener) captures props and state from the render it was created in. If those values change later, the effect has a stale snapshot — a classic stale closure bug.

The fix: mirror the latest value in a ref that's updated every render. The long-lived callback reads ref.current instead of the closed-over snapshot.

function useInterval(callback, delay) {
  const callbackRef = useRef(callback)
  useEffect(() => { callbackRef.current = callback }) // always current

  useEffect(() => {
    const id = setInterval(() => callbackRef.current(), delay)
    return () => clearInterval(id)
  }, [delay]) // interval created once; callback is always fresh via ref
}

This is the foundation of the useEventCallback pattern used in many hook libraries. It avoids tearing down and recreating expensive subscriptions on every callback change.

Callback refs — for dynamic or conditional elements

A regular ref object doesn't notify you when it gets attached to a node. A callback ref (a function passed to the ref attribute) fires with the DOM node on mount and null on unmount — exactly when you need it.

function MeasureDiv() {
  const [height, setHeight] = useState(null)

  const measuredRef = useCallback(node => {
    if (node) setHeight(node.getBoundingClientRect().height)
  }, [])

  return <div ref={measuredRef}>content — {height}px tall</div>
}

Use callback refs when: the element is conditionally rendered, the number of elements changes, or you need to run a side effect the instant the node is available.

forwardRef — exposing a child's DOM node to the parent

Function components can't receive a ref prop by default (there's no instance for it to point to). Wrap the component in forwardRef to let a parent reach a DOM node inside.

const TextInput = forwardRef(function TextInput({ label }, ref) {
  return (
    <label>
      {label}
      <input ref={ref} type="text" />
    </label>
  )
})

function Form() {
  const inputRef = useRef(null)
  return (
    <>
      <TextInput label="Name" ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  )
}

forwardRef is the correct API in React 18 and below. React 19 makes ref a plain prop on function components, eliminating the need for forwardRef.

useImperativeHandle — controlling the exposed API

By default, forwardRef exposes the raw DOM node. useImperativeHandle lets you expose a custom API instead — useful for encapsulating behavior and hiding implementation details.

const Dialog = forwardRef(function Dialog(props, ref) {
  const [open, setOpen] = useState(false)

  useImperativeHandle(ref, () => ({
    open:  () => setOpen(true),
    close: () => setOpen(false),
  }))

  return open ? <div role="dialog">{props.children}</div> : null
})

function App() {
  const dialogRef = useRef(null)
  return (
    <>
      <button onClick={() => dialogRef.current.open()}>Open dialog</button>
      <Dialog ref={dialogRef}>Hello</Dialog>
    </>
  )
}

This is an escape hatch for legitimately imperative APIs (animations, focus sequences, media controls). Keep it narrow — expose methods, not state.

Per-instance vs. module-level variables

A module-level let is shared across all instances of the component. A ref is per-instance — each mounted copy has its own .current.

let sharedTimer = null  // shared! every Poller reads the same variable

function Poller() {
  const timerRef = useRef(null) // isolated; each Poller owns its timer
  // ...
}

Whenever you're tempted to reach for a module-level variable to persist something across renders, ask: does it need to be per-instance? If yes, use a ref.

Common interview questions at a glance

  • What does useRef return? A mutable object { current: initialValue } that persists for the component's lifetime. Mutations don't cause re-renders.
  • How does useRef differ from useState? Mutations are invisible to React — no re-render. Use refs for invisible bookkeeping, state for things that appear in the UI.
  • When is ref.current null? During the first render (before mount) and after unmount. DOM refs are only safe inside effects and event handlers.
  • What is forwardRef? A wrapper that lets a parent pass a ref into a function component so it can attach to an internal DOM node.
  • What is useImperativeHandle? Customizes the value the parent's ref receives — exposing a method API instead of a raw DOM node.
  • What is the callback ref pattern? Passing a function to ref instead of a ref object, so you get notified the moment a DOM node mounts or unmounts.
  • What is the "latest value" ref pattern? Mirroring a prop/state into a ref each render so a long-lived callback always reads fresh data without stale closures.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel