BeginnerReact Concepts

Refs vs State in React – Quiet Memory vs Reactive Updates

Both refs and state are ways to store data in React, but they behave differently. This comparison explains when to use state (for UI) vs. when to use refs (for DOM access or hidden data), with clear real-world examples.

By Rudraksh Laddha

Three months into my first React job, I made a mistake that crashed our entire form validation system. I was storing a timer ID in state instead of a ref. Every keystroke triggered unnecessary re-renders, and the app ground to a halt.

Here's the production lesson I learned the hard way:

  • State is for values that should update what users see
  • Ref is for values that need to persist but shouldn't trigger renders

The Performance Impact You Need to Know

Feature State Ref
Triggers re-render? ✅ Yes ❌ No
Best for UI values that must appear or update Behind-the-scenes memory or DOM refs
Accessed via state and setState() ref.current
React involvement Fully reactive Outside of render logic
Suitable for DOM? ❌ No ✅ Yes
Use case style Display & UI state Temporary values, API handles

The Code That Taught Me This Lesson

✅ State: When Users Need to See Changes

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

// Every click updates UI immediately
<button onClick={() => setCount(count + 1)}>{count}</button>

✅ Ref: When You Need Memory Without Renders

const countRef = useRef(0);

// Increments silently - no UI update, no re-render
<button onClick={() => (countRef.current += 1)}>Click</button>

The ref version won't show the count on screen, but it's tracking every click without the performance cost of re-rendering. Perfect for analytics or debugging.


My Decision Framework: State vs Ref

I choose state when:

  • The value directly affects what users see
  • Other components need to react to changes
  • I need React's built-in batching and optimization

I choose ref for:

  • DOM manipulation (focus, scroll, canvas operations)
  • Timer IDs and interval cleanup
  • Previous values for comparison logic
  • Third-party library instances (maps, charts, editors)

Production Bug: Timer Management

❌ What I did wrong (caused memory leaks):

const [timeoutId, setTimeoutId] = useState();

// This triggers unnecessary re-renders and complicates cleanup
setTimeoutId(setTimeout(() => doSomething(), 1000));

✅ The fix that saved our performance:

const timeoutRef = useRef();

// Clean, efficient, no re-renders
timeoutRef.current = setTimeout(() => doSomething(), 1000);

// Easy cleanup in useEffect
useEffect(() => {
  return () => clearTimeout(timeoutRef.current);
}, []);

This pattern eliminated 200+ unnecessary re-renders in our search component. The ref stores the timeout ID without triggering React's reconciliation process.


Real-World DOM Control

const inputRef = useRef();
const scrollContainerRef = useRef();

useEffect(() => {
  // Focus management for accessibility
  inputRef.current.focus();
  
  // Scroll to bottom for chat components
  scrollContainerRef.current.scrollTop = 
    scrollContainerRef.current.scrollHeight;
}, [messages]);

✅ Direct DOM manipulation without state changes. This is how we implement focus traps, scroll restoration, and canvas drawing operations in production apps.


Storing Complex Objects: The Right Way

For WebSocket connections, file readers, or third-party library instances:

// ❌ Never store these in state
const [mapInstance, setMapInstance] = useState();
const [websocket, setWebsocket] = useState();

// ✅ Use refs for objects React shouldn't track
const mapRef = useRef();
const websocketRef = useRef();

useEffect(() => {
  mapRef.current = new google.maps.Map(mapElement, options);
  websocketRef.current = new WebSocket(url);
}, []);

State would try to serialize these objects and trigger re-renders on every property change. Refs let you manage them outside React's lifecycle.


My Production Rule

Ask yourself: "If this value changes, should React re-render the component?"
Yes? Use State.
No? Use Ref.


✅ Quick Reference: When I Choose Each

Scenario State Ref
Form input values ✅ For controlled inputs ✅ For uncontrolled inputs
Toggle switches, counters ✅ Always ❌ Never
Timer/interval IDs ❌ Never ✅ Always
DOM element references ❌ Never ✅ Always
Previous prop values ❌ Rarely ✅ Perfect use case
API response data ✅ If displayed ✅ If just stored

Questions I Get from My Development

1. "Can I use ref for caching expensive calculations?"

Yes, but useMemo is usually better. I use refs when the cached value needs to persist across unmount/remount cycles or when dealing with non-serializable objects.

2. "Why can't I just use a regular variable instead of useRef?"

Regular variables get reset on every render. Refs persist their values between renders, making them perfect for storing mutable data that doesn't affect the UI.

3. "Is it safe to mutate ref.current directly?"

Yes, that's exactly what refs are for. Unlike state, you can directly assign to ref.current without any setter function.

4. "Can refs cause memory leaks?"

They can if you store objects with event listeners or timers. Always clean up in useEffect return functions, especially for DOM event listeners and intervals.

5. "Should I use refs for form validation?"

Depends on your approach. For real-time validation that shows errors, use state. For submit-time validation or third-party form libraries, refs work great.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers