React Interview Prep
Hooks

useRef

Escape Hatch

LinkedIn Hook

You store a value in useRef, update it, and... nothing happens. No re-render. No UI change.

That's not a bug. That's the entire point.

Most React developers reach for useRef only when they need to focus an input. But refs are far more powerful than DOM access. They're your escape hatch from React's rendering cycle — a way to hold onto values that survive re-renders without causing them.

In interviews, useRef questions separate developers who memorize hooks from those who truly understand React's mental model. When should you use a ref vs state? How do you track a previous value? What is forwardRef, and why does useImperativeHandle exist?

In this lesson, I cover all of it — DOM access, mutable containers, the previous value pattern, forwarding refs, and useImperativeHandle — with real code and interview-ready explanations.

If you've ever wondered why React has an "escape hatch" from its own system — this one's for you.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #CodingInterview #ReactHooks #useRef #100DaysOfCode


useRef thumbnail


What You'll Learn

  • What useRef is and how it differs from useState
  • How to access and manipulate DOM elements directly
  • How to persist values between renders without triggering re-renders
  • The previous value pattern — tracking what something was before the last render
  • How to forward refs to child components with forwardRef
  • How to customize exposed ref methods with useImperativeHandle

The Concept — What Is useRef?

Analogy: The Sticky Note on Your Monitor

Imagine your React component is an employee who gets a fresh desk every morning (every re-render). Everything on the desk — local variables — gets cleared and rebuilt. State is like the company filing cabinet: updating it triggers a notification to everyone (re-render).

But sometimes you just need a sticky note on your monitor. Something personal. Something that survives the desk reset. Something you can scribble on without sending a company-wide email.

That's useRef. It gives you a mutable object { current: value } that:

  • Persists across re-renders (doesn't reset like a local variable)
  • Does NOT trigger a re-render when you change it (unlike state)
  • Is your "escape hatch" when you need to step outside React's declarative model

How useRef Works

useRef returns a plain JavaScript object with a single property: current. React guarantees this object stays the same between renders.

const myRef = useRef(initialValue);
// myRef = { current: initialValue }
// You can read: myRef.current
// You can write: myRef.current = newValue
// Changing .current does NOT cause a re-render

Use Case 1: DOM Access

The most common use of useRef — getting a direct reference to a DOM element.

Code Example 1: Focus an Input on Mount

import { useRef, useEffect } from "react";

function SearchBar() {
  // Create a ref to hold the input DOM element
  const inputRef = useRef(null);

  useEffect(() => {
    // After the component mounts, focus the input
    inputRef.current.focus();
  }, []);

  return (
    <div>
      {/* Attach the ref to the input element */}
      <input ref={inputRef} type="text" placeholder="Search..." />
    </div>
  );
}

// When the component mounts:
// 1. React renders the <input> to the DOM
// 2. React sets inputRef.current = the actual <input> DOM node
// 3. useEffect runs, calling .focus() on the real DOM element
// 4. The input is focused automatically — no document.getElementById needed

Key points:

  • Pass ref={myRef} to a JSX element, and React fills myRef.current with the DOM node
  • The ref is null during the first render (the DOM doesn't exist yet), then gets populated after mount
  • Use refs for focus, scroll, measuring dimensions, or integrating with non-React libraries

Use Case 2: Persisting Values Without Re-Renders

This is the use case most developers miss. Refs are mutable containers that survive re-renders — perfect for values you need to track but don't want to display.

Code Example 2: Counting Renders

import { useRef, useState } from "react";

function RenderCounter() {
  const [name, setName] = useState("");
  // Track how many times this component has rendered
  const renderCount = useRef(0);

  // This runs on every render (no useEffect needed)
  renderCount.current += 1;

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Type your name"
      />
      <p>Name: {name}</p>
      <p>This component has rendered {renderCount.current} times.</p>
    </div>
  );
}

// User types "A":
// Render 1: Name: "", renderCount: 1
// Render 2: Name: "A", renderCount: 2
//
// If we used useState for renderCount instead:
// Updating it would trigger ANOTHER re-render, causing an infinite loop!
// useRef avoids this because changing .current doesn't trigger re-renders.

useRef visual 1


Use Case 3: The Previous Value Pattern

A classic interview pattern: tracking the previous value of a prop or state. Since refs persist without re-rendering, they're perfect for this.

Code Example 3: Tracking Previous State

import { useRef, useState, useEffect } from "react";

// Custom hook to get the previous value of anything
function usePrevious(value) {
  const prevRef = useRef();

  useEffect(() => {
    // After each render, store the current value for next time
    prevRef.current = value;
  }, [value]);

  // During this render, prevRef.current still holds the OLD value
  // (useEffect hasn't run yet — it runs AFTER render)
  return prevRef.current;
}

function PriceTracker({ price }) {
  const previousPrice = usePrevious(price);

  return (
    <div>
      <p>Current price: ${price}</p>
      <p>Previous price: ${previousPrice}</p>
      {previousPrice !== undefined && (
        <p>{price > previousPrice ? "Price went UP" : "Price went DOWN"}</p>
      )}
    </div>
  );
}

// price changes: 10 -> 15 -> 12
// Render 1: Current: $10, Previous: undefined (first render)
// Render 2: Current: $15, Previous: $10 — "Price went UP"
// Render 3: Current: $12, Previous: $15 — "Price went DOWN"
//
// Why this works:
// 1. Component renders with new `price`
// 2. During render, prevRef.current still holds the OLD value
// 3. After render, useEffect fires and updates prevRef.current to the new value
// 4. Next render, prevRef.current holds what was "new" last time = the previous value

Why not use state for this? Updating state would trigger another re-render, which would update the previous value again, which would trigger another re-render... infinite loop. Refs break the cycle because they update silently.


Use Case 4: Forwarding Refs with forwardRef

Regular function components can't receive a ref prop — React intercepts it. To pass a ref to a child component's inner DOM element, you wrap the child with forwardRef.

Code Example 4: forwardRef and useImperativeHandle

import { useRef, forwardRef, useImperativeHandle } from "react";

// forwardRef wraps the component and passes the ref as a second argument
const FancyInput = forwardRef(function FancyInput(props, ref) {
  const localInputRef = useRef(null);

  // useImperativeHandle customizes what the parent sees through the ref
  // Instead of exposing the entire DOM node, expose only specific methods
  useImperativeHandle(ref, () => ({
    // Parent can call ref.current.focusAndClear()
    focusAndClear() {
      localInputRef.current.focus();
      localInputRef.current.value = "";
    },
    // Parent can call ref.current.scrollToView()
    scrollToView() {
      localInputRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }));

  return <input ref={localInputRef} {...props} />;
});

function ParentForm() {
  const inputRef = useRef(null);

  function handleReset() {
    // The parent doesn't get the raw <input> DOM node
    // It only gets the methods we defined in useImperativeHandle
    inputRef.current.focusAndClear();
  }

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Enter email" />
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

// When the user clicks "Reset":
// 1. handleReset calls inputRef.current.focusAndClear()
// 2. focusAndClear focuses the input and clears its value
//
// The parent never touches the raw DOM node directly.
// useImperativeHandle acts as a controlled API — exposing only what you choose.

Why useImperativeHandle? It follows the principle of least privilege. Instead of handing the parent full DOM access (which could lead to unsafe mutations), you expose a clean, intentional API. Interviewers love this pattern because it shows you understand encapsulation in React.

useRef visual 2


Common Mistakes

Mistake 1: Using ref.current in the render output and expecting re-renders

function BadComponent() {
  const countRef = useRef(0);

  function handleClick() {
    countRef.current += 1;
    // The ref value changed, but React doesn't know!
    // No re-render happens, so the UI stays stale.
  }

  return <p>Count: {countRef.current}</p>; // This never updates on screen!
}

// FIX: If the value should appear in the UI, use useState instead.
// useRef is for values you need to track silently behind the scenes.

Mistake 2: Reading ref.current during render for DOM refs

function BadDOMAccess() {
  const divRef = useRef(null);

  // This runs during render — the DOM doesn't exist yet!
  const width = divRef.current?.offsetWidth; // Always undefined/null on first render

  return <div ref={divRef}>Hello</div>;
}

// FIX: Access DOM refs inside useEffect or event handlers — 
// that's when the DOM node actually exists.

Mistake 3: Forgetting forwardRef when passing refs to custom components

// WRONG — ref is silently dropped, ref.current stays null
function MyInput(props) {
  return <input {...props} />;
}

// RIGHT — forwardRef passes the ref through to the DOM element
const MyInput = forwardRef(function MyInput(props, ref) {
  return <input ref={ref} {...props} />;
});

Interview Questions

Q: What is the difference between useRef and useState?

Both persist values across re-renders. The key difference: updating state (setState) triggers a re-render, while mutating ref.current does not. Use state for values that should be reflected in the UI. Use refs for values you need to track silently (timers, previous values, DOM nodes) or when you need direct DOM access.

Q: Can you update a ref during rendering? Is it safe?

You can read and write ref.current during rendering for non-DOM refs (like a render counter), but you should NOT read DOM refs during render because the DOM element may not exist yet. DOM refs should be accessed in useEffect or event handlers. Also, be aware that changing a ref during render won't cause the UI to reflect that change until the next re-render triggered by something else.

Q: How would you implement a custom hook that tracks the previous value of a prop?

Use a ref combined with useEffect. During render, the ref still holds the old value (useEffect hasn't run yet). After render, useEffect updates the ref to the current value. On the next render, the ref holds what was current last time — the previous value. This avoids the infinite loop that would occur with state.

Q: What is forwardRef and why is it needed?

React doesn't pass ref as a regular prop to function components — it's intercepted and handled specially. forwardRef is a wrapper that lets a function component receive a ref from its parent and attach it to an inner DOM element. Without it, the parent's ref would be null. It's essential for building reusable component libraries where consumers need DOM access.

Q: What does useImperativeHandle do, and when would you use it?

useImperativeHandle customizes the value exposed to a parent via ref. Instead of exposing the raw DOM node, you define a specific set of methods (like focus(), reset(), scrollTo()). This follows the principle of least privilege — the parent only gets access to what you explicitly allow. Use it when building reusable components that need to expose imperative actions without leaking internal DOM details.


Quick Reference — Cheat Sheet

+-----------------------------------+-------------------------------------------+
| Concept                           | Key Point                                 |
+-----------------------------------+-------------------------------------------+
| useRef(initialValue)              | Returns { current: initialValue }.        |
|                                   | Same object on every render.              |
+-----------------------------------+-------------------------------------------+
| ref.current                       | Mutable. Changing it does NOT trigger     |
|                                   | a re-render. Persists across renders.     |
+-----------------------------------+-------------------------------------------+
| DOM access                        | Pass ref={myRef} to a JSX element.        |
|                                   | Access the node via myRef.current.        |
+-----------------------------------+-------------------------------------------+
| Persist value (no re-render)      | Use useRef for timers, counters,          |
|                                   | previous values, interval IDs.            |
+-----------------------------------+-------------------------------------------+
| Previous value pattern            | useEffect updates ref.current AFTER       |
|                                   | render. During render, ref holds old val. |
+-----------------------------------+-------------------------------------------+
| forwardRef                        | Wraps a component so it can receive       |
|                                   | a ref from its parent.                    |
+-----------------------------------+-------------------------------------------+
| useImperativeHandle(ref, () => {})| Customizes what the parent sees via ref.  |
|                                   | Expose methods, not the raw DOM node.     |
+-----------------------------------+-------------------------------------------+

RULE: If the value drives UI → useState.
RULE: If the value is silent/internal → useRef.
RULE: DOM refs → access in useEffect or event handlers, never during render.

Previous: Lesson 3.2 — useEffect — Side Effects -> Next: Lesson 3.4 — useMemo & useCallback ->


This is Lesson 3.3 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page