Reach for ref Instead of useState Whenever You Can!

Sun, June 1, 2025 - 2 min read
React hooks: useRef and useState

⚡ Reach for ref Instead of useState Whenever You Can!

We all love useState: straightforward, declarative, reliable. Yet that reliable re-render is exactly what bites when you only need to stash a value. 👀

Let’s walk through the moments when useRef keeps your component snappy by skipping unnecessary updates.


Why useState Isn’t Always the Hero

🌀 Every change triggers a render

Whenever you set state, React rerenders the component. For controlled inputs that’s fine. But for timers, interim values, or DOM handles it’s just extra work.

🧠 State should describe the UI

If a value never appears in the markup, it probably shouldn’t live in state. “Previous request ID” has nothing to do with what the user sees — pop it into a ref instead.


Where ref Wins Over useState

1. 🔄 Timers, intervals, execution handles

const timerRef = useRef<number | null>(null);
 
useEffect(() => {
  timerRef.current = window.setInterval(() => {
    // ... interval logic
  }, 1000);
 
  return () => {
    if (timerRef.current !== null) {
      window.clearInterval(timerRef.current);
    }
  };
}, []);

The timer ID persists between renders, but doesn’t touch the UI. ref keeps the component quiet and lets us clean up on unmount.

2. 💬 Values that must survive renders

const previousQueryRef = useRef<string>('');
 
useEffect(() => {
  previousQueryRef.current = query;
}, [query]);
 
const isRepeated = previousQueryRef.current === query;

We need the history for logic, not for rendering. ref is a perfect fit.

3. 🧭 Accessing the DOM without rerenders

const inputRef = useRef<HTMLInputElement>(null);
 
const focus = () => {
  inputRef.current?.focus();
};

useRef gives a stable pointer to a DOM node. Focus it, scroll it, measure it — no virtual DOM diff required.

4. ⚙️ External APIs and third-party widgets

const chartInstanceRef = useRef<Chart | null>(null);
 
useEffect(() => {
  chartInstanceRef.current = initChart(canvasElement);
  return () => chartInstanceRef.current?.destroy();
}, []);

A chart instance, map object, or media player can be stored in ref. Keep the UI calm while the library does its job.


When useState Still Makes Sense

  • 🎚️ Controlled inputs (value, checked)
  • 🌡️ UI configuration (is modal open, which tab is active)
  • 📊 Data that literally shapes the JSX

If it changes the visual output, let it live in state. Predictability beats micro-optimization here.


Quick Checklist: state or ref?

TaskuseStateuseRef
Controlled UI🚫
DOM references🚫
Timers & intervals🚫
External instances (charts, maps)🚫
Caching values between renders🚫

Takeaway

useRef isn’t a dumping ground. It’s a conscious choice to separate reactive data from plain storage. If the value doesn’t touch the UI, shove it into a ref and keep the component blazing fast. 🚀