Beginner0 questionsFull Guide

useRef Hook — Complete React Interview Guide

Master useRef for DOM access, mutable values, and breaking stale closures — without triggering re-renders.

The Mental Model

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.

The Explanation

What useRef returns and how it works

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

Use case 1 — DOM access

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>
}

Use case 2 — Mutable instance variables (no re-render)

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>
  )
}

Breaking stale closures with useRef

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), [])
}

Tracking previous values with useRef

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>
}

forwardRef — passing refs to child components

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>
    </>
  )
}

useImperativeHandle — controlling the exposed API

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} />
})

Callback refs — for conditional or dynamic elements

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
}

useRef vs useState — when to choose which

ScenarioUseWhy
Counter to display to useruseStateUI must update
Interval/timer IDuseRefNo UI update needed
DOM node accessuseRefImperative DOM API
Previous value trackinguseRefSilent, no re-render
Form input value (controlled)useStateValue drives UI
Form input value (submit only)useRefRead on demand
WebSocket instanceuseRefInstance variable
isMounted flaguseRefSilent flag

Common Misconceptions

⚠️

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.

Where You'll See This in Real Code

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.

Interview Cheat Sheet

  • useRef(initialValue) returns { current: initialValue } — same object persists for component lifetime
  • Mutating ref.current does NOT trigger a re-render — completely invisible to React
  • Use for DOM access: attach to JSX via ref={myRef}, access node in useEffect or event handlers
  • ref.current is null during render — DOM is only available after mount
  • Use for instance variables: timer IDs, WebSocket instances, subscription objects
  • usePrevious pattern: store value in useEffect (post-render) — ref.current = previous value during current render
  • Stale closure fix: store latest value in ref via useEffect, read ref.current in callbacks
  • forwardRef: lets parent's ref attach to DOM inside a child component
  • useImperativeHandle: control what methods the parent can call on the forwarded ref
  • Callback ref (function as ref): called with DOM node on mount, null on unmount — fires immediately on conditional renders
  • createRef vs useRef: createRef creates new object every render — never use in functional components
  • React 19: forwardRef becomes unnecessary — refs are plain props
💡

How to Answer in an Interview

  • 1.When asked "what is useRef", give the two-sentence precise answer: "useRef returns a mutable {current} object that persists for the component's lifetime. Mutating current never triggers a re-render." Then distinguish the two use cases: DOM access and instance variables. Most candidates only mention DOM access — mentioning both immediately separates you.
  • 2.The stale closure + useRef pattern is a senior-level question asked at Atlassian, Razorpay, and CRED. Prepare the "always-current callback" pattern: store fn in ref → update in useEffect → expose stable useCallback wrapper. This shows you understand both stale closures AND ref semantics at the same time.
  • 3.When asked about forwardRef, connect it to a real use case: "I've used forwardRef when building a design system's Input component — the consuming page needs to programmatically focus the input, but the ref should attach to the actual DOM input inside the component, not the wrapper div." Real scenario + explanation = senior signal.
  • 4.The usePrevious pattern is frequently asked as an implementation question. Explain the timing: "useEffect runs after render. During render N, ref.current still holds render N-1's value. After render N, the effect updates the ref for render N+1." Draw the timeline on a whiteboard if you're in person.
  • 5.When comparing useRef vs useState, give the decisive rule: "If the UI needs to update when the value changes — state. If not — ref." Then give a concrete example of something that looks like it needs state but actually shouldn't cause a re-render: tracking whether a WebSocket is open, storing a scroll position, or holding a debounce timer ID.

Practice Questions

No questions tagged to this topic yet.

Related Topics

useState Hook — Complete React Interview Guide
Beginner·4–8 Qs
useEffect Hook — Complete React Interview Guide
Beginner·4–8 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz