Skip to content

useRef Interview Questions & Answers

15 questions Updated 2026-06-23 Share:

React useRef interview questions — accessing DOM elements, mutable instance variables, forwardRef, useImperativeHandle, callback refs, and ref vs state.

Read the in-depth guideReact useRef Hook — Complete Interview Guide with Examples(opens in new tab)
15 of 15

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 ways to practice

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

or
Join our WhatsApp Channel