useRef from Basic to Best Practice

Understand how useRef works, when to use it instead of state, how refs interact with renders, and the patterns that make them useful in real React applications.


1. What Is useRef?

Start with the simplest possible example:

const countRef = useRef(0);

Explain that useRef gives you a mutable object whose .current value persists between renders.

Then introduce the core mental model:

useState is for data that affects rendering. useRef is for data that needs to persist without triggering a render.

Show a simple example:

const renderCount = useRef(0);
 
renderCount.current += 1;

Explain that changing current does not cause React to render again.


2. The Mental Model: useRef Survives Renders

Explain that every render creates new local variables:

function Component() {
  let value = 0;
}

The variable doesn't persist between renders.

But:

const value = useRef(0);

does.

The same ref object remains available across renders:

Render 1
   ↓
ref.current = 0
   ↓
Render 2
   ↓
same ref object
   ↓
ref.current = 1

This is the first major concept readers should understand.


3. useRef vs useState

This should be one of the most important sections.

Compare:

const [count, setCount] = useState(0);

with:

const countRef = useRef(0);

Explain the difference:

useStateuseRef
Persists between rendersYesYes
Changing value causes renderYesNo
Value accessed throughvariable.current
Intended for UI stateYesNo
Can hold DOM referencesNoYes

Then show the practical rule:

If changing the value should update the UI, use state.
If the value needs to persist but shouldn't update the UI, consider a ref.


4. The Most Common Use Case: DOM References

Introduce the classic example:

const inputRef = useRef<HTMLInputElement>(null);
 
return (
  <>
    <input ref={inputRef} />
    <button onClick={() => inputRef.current?.focus()}>
      Focus input
    </button>
  </>
);

Explain what React does when the element mounts and why current initially contains null.

This naturally introduces:


5. Why Changing .current Doesn't Re-render

This is the useRef equivalent of the reference article's “state doesn't update immediately” section.

Show:

const countRef = useRef(0);
 
const handleClick = () => {
  countRef.current += 1;
  console.log(countRef.current);
};

The value changes immediately, but the component doesn't re-render.

Then demonstrate why this doesn't update the UI:

<p>{countRef.current}</p>

The value can change internally while the displayed UI remains unchanged.

This is an important distinction:

A ref stores a value. It does not tell React that the UI needs to change.


6. Don't Use useRef as a Replacement for State

Show a tempting but incorrect pattern:

const count = useRef(0);
 
const increment = () => {
  count.current += 1;
};

If the UI displays:

<p>{count.current}</p>

the screen won't update.

Then show the correct version:

const [count, setCount] = useState(0);
 
const increment = () => {
  setCount(prev => prev + 1);
};

The rule should be clear:

If the value affects what you render, it probably belongs in state.


7. Storing Values Between Renders

Now move beyond DOM elements.

Explain that refs are useful for values such as:

Example:

const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
const startTimer = () => {
  timerRef.current = setTimeout(() => {
    console.log("Done");
  }, 1000);
};

The timer ID needs to survive renders, but changing it doesn't need to update the UI.


8. Tracking the Previous Value

A very practical pattern:

const previousValue = useRef(value);
 
useEffect(() => {
  previousValue.current = value;
}, [value]);

Then:

console.log(previousValue.current);

Explain why this works and why state would introduce unnecessary rendering in this particular case.

This is a good place to discuss the difference between:


9. useRef and Closures

This should be one of the more advanced sections.

Show a callback that captures an old value:

const [count, setCount] = useState(0);
 
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
 
  return () => clearInterval(id);
}, []);

Then introduce a ref as a way to keep access to the latest value:

const countRef = useRef(count);
 
useEffect(() => {
  countRef.current = count;
}, [count]);

Now asynchronous code can read:

console.log(countRef.current);

Explain the important distinction:

useRef doesn't magically fix closures. It gives you a mutable location that asynchronous callbacks can read from later.


10. Managing Timers and Intervals

Build a realistic example:

const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
const start = () => {
  intervalRef.current = setInterval(() => {
    // ...
  }, 1000);
};
 
const stop = () => {
  if (intervalRef.current) {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }
};

Explain why this belongs in a ref instead of state.

The timer ID is an implementation detail, not something the user needs rendered.


11. useRef for Third-Party Libraries

Show how refs can hold objects created by libraries:

const editorRef = useRef<Editor | null>(null);

Then:

editorRef.current = new Editor(...);

This pattern is especially useful for:

The key idea:

React manages the UI. The ref can hold an imperative object that exists alongside that UI.


12. Callback Refs vs useRef

Introduce callback refs briefly:

<div
  ref={node => {
    // node is available here
  }}
/>

Explain when callback refs can be useful compared with:

const elementRef = useRef<HTMLDivElement>(null);

Don't make this section too deep unless the article is aimed at experienced React developers.


13. useRef Initialization

Discuss:

const instanceRef = useRef(new ExpensiveThing());

versus lazily creating an object when needed.

For objects that should only be created once, explain why blindly putting an expensive constructor into the argument can still execute that expression during rendering.

A safer pattern can be:

const instanceRef = useRef<ExpensiveThing | null>(null);
 
if (instanceRef.current === null) {
  instanceRef.current = new ExpensiveThing();
}

This is a good advanced best-practice section.


14. Common useRef Mistakes

Create a section similar to the reference article's “Common Mistakes to Avoid.”

Mistake 1: Using refs for UI state

const valueRef = useRef(0);

when the value should appear on screen.

Mistake 2: Expecting .current changes to trigger rendering

ref.current = newValue;

doesn't cause a render.

Mistake 3: Reading or writing refs during render unnecessarily

Refs are primarily for values that live outside React's reactive rendering flow.

Mistake 4: Forgetting cleanup

Especially for:

Mistake 5: Using refs to avoid understanding state

A ref shouldn't become an escape hatch for every state-management problem.


15. useRef vs useState vs useMemo

A useful comparison for the final part:

useState

Use when:

The value affects the rendered UI.

useRef

Use when:

The value must persist but changing it doesn't need to re-render.

useMemo

Use when:

You want to cache an expensive calculation between renders.

Make it clear that these hooks solve different problems rather than being interchangeable optimization tools.


16. Putting Everything Together

End with a realistic component that combines several concepts:

For example:

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const previousSearch = useRef("");
 
  const [search, setSearch] = useState("");
 
  const handleSearch = (value: string) => {
    previousSearch.current = search;
    setSearch(value);
 
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
 
    timerRef.current = setTimeout(() => {
      console.log("Searching:", value);
    }, 300);
  };
 
  return (
    <div>
      <input
        ref={inputRef}
        value={search}
        onChange={event => handleSearch(event.target.value)}
      />
 
      <button onClick={() => inputRef.current?.focus()}>
        Focus
      </button>
    </div>
  );
}

Then explain what each piece is responsible for.


17. Best Practices Checklist

Before adding a ref, ask:

A simple rule:

State drives the UI. Refs store mutable values that exist outside the UI.


Conclusion

useRef looks like a small Hook, but understanding it requires understanding one of React's most important ideas: not every value in a component needs to participate in rendering.

Once you understand that distinction, useRef becomes much easier to reason about.

You'll know when to use refs for DOM elements, timers, previous values, external instances, and asynchronous callbacks.

More importantly, you'll know when not to use them.

The goal isn't to use useRef more often.

It's to understand exactly when a value belongs in React's rendering system—and when it doesn't.