BeginnerReact Concepts

useRef in React: The Silent Worker You Need to Master

useRef stores values that persist across renders without triggering re-renders — often used to access DOM elements or store silent variables.

By Rudraksh Laddha

A months ago, I was debugging a file upload component that kept losing track of ongoing uploads during re-renders. Every time the user clicked somewhere else, the progress would reset. The culprit? Using useState for tracking upload IDs that should never trigger UI updates.

"Not everything in React needs to trigger a re-render. Sometimes you need memory that just... remembers."

That's exactly what useRef solves — persistent memory and DOM access without the re-render overhead.

What useRef Actually Does (Beyond the Docs)

useRef creates a container that survives component re-renders. Think of it as a backpack that follows your component around:

  1. Stores mutable values that persist between renders
  2. Provides direct access to DOM elements when you need to break out of React's declarative model

The key insight: ref.current changes don't schedule re-renders. This makes it perfect for side effects and imperative operations.

The Basic Pattern

const myRef = useRef(initialValue);

Returns: { current: initialValue }

// Access and modify
myRef.current = newValue;
console.log(myRef.current);

No magic here — just a plain object with a current property that React keeps stable across renders.

Pattern 1: DOM Manipulation When You Must

const AutoFocusInput = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    // Focus after component mounts
    inputRef.current?.focus();
  }, []);

  const handleClear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input 
        ref={inputRef} 
        placeholder="Auto-focused on mount" 
      />
      <button onClick={handleClear}>Clear & Focus</button>
    </div>
  );
};

Real-world usage: I use this pattern for search inputs in dashboards where users expect immediate focus, and for clearing forms while maintaining focus flow. The optional chaining (?.) prevents errors if the ref isn't attached yet.

Pattern 2: Tracking Without Triggering

const PerformanceTracker = () => {
  const [data, setData] = useState([]);
  const renderCount = useRef(0);
  const startTime = useRef(Date.now());

  // This runs on every render but doesn't cause new renders
  renderCount.current += 1;

  const fetchData = async () => {
    const response = await fetch('/api/data');
    const result = await response.json();
    setData(result);
  };

  return (
    <div>
      <p>Data items: {data.length}</p>
      <p>Renders: {renderCount.current}</p>
      <p>Uptime: {Math.floor((Date.now() - startTime.current) / 1000)}s</p>
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  );
};

Why this works: Performance metrics shouldn't cause re-renders themselves. I've used similar patterns to track user interactions for analytics without impacting the render cycle.

When to Reach for useRef

After building dozens of React apps, I reach for useRef in these situations:

  • DOM imperative operations — focus management, scroll position, canvas drawing
  • Storing interval/timeout IDs — cleanup without causing renders
  • Tracking previous values — comparing props or state across renders
  • Caching expensive computations — when useMemo isn't enough
  • Third-party library integration — storing library instances

Red flag: If changing the value should update the UI, use useState instead. Don't fight React's rendering model.

useRef vs useState: Choose Your Fighter

Aspect useRef useState
Triggers re-render? ❌ Never ✅ Always
DOM access? ✅ Perfect for it ❌ Wrong tool
Async cleanup? ✅ Ideal ❌ Overkill
UI updates? ❌ Won't work ✅ Designed for this
Performance impact? ✅ Zero ⚠️ Causes renders

Advanced Pattern: Cleanup That Actually Works

const NotificationManager = () => {
  const timeoutIds = useRef(new Set());

  const showNotification = (message, duration = 3000) => {
    const id = setTimeout(() => {
      setNotifications(prev => 
        prev.filter(n => n.id !== notificationId)
      );
      timeoutIds.current.delete(id);
    }, duration);
    
    timeoutIds.current.add(id);
  };

  useEffect(() => {
    // Cleanup all pending timeouts on unmount
    return () => {
      timeoutIds.current.forEach(id => clearTimeout(id));
      timeoutIds.current.clear();
    };
  }, []);

  return (
    <button onClick={() => showNotification('Hello!')}>
      Show Notification
    </button>
  );
};

Production lesson: I learned this pattern the hard way when notifications kept firing after component unmount, causing memory leaks and React warnings. Storing cleanup references in useRef prevents these issues.

Common Pitfalls I've Debugged

Mistake What Happens Solution
Using ref for UI state UI doesn't update when value changes Use useState for anything that affects rendering
Forgetting to check current "Cannot read property of null" errors Always use optional chaining: ref.current?.method()
Setting ref in render Infinite re-render loops Update refs in effects or event handlers only
Sharing refs between components Unpredictable behavior and hard-to-track bugs Keep refs scoped to single components

useRef Mastery Checklist

Concept Key Insight
Core purpose Persistent storage that doesn't trigger renders
Re-render behavior Updating .current never causes re-renders
Best use cases DOM access, timers, previous values, library instances
Basic syntax const ref = useRef(initialValue)
Access pattern ref.current with optional chaining for safety
Cleanup strategy Store cleanup functions/IDs in refs, clear in useEffect

useRef Questions I Get Asked

When should I use useRef instead of useState?

Use useRef when the value change shouldn't trigger a re-render. If users need to see the change immediately, use useState. I decide by asking: "Does this change affect what the user sees?"

Can useRef cause memory leaks?

Yes, if you store references to DOM elements or functions without proper cleanup. Always clear refs in useEffect cleanup functions when storing timeouts, intervals, or event listeners.

Is it safe to mutate ref.current?

Absolutely — that's the point. Unlike state, refs are designed to be mutated directly. Just don't mutate them during render (do it in effects or event handlers).

Can I use useRef for form data?

Only for uncontrolled components where React doesn't manage the form state. For most forms, controlled components with useState provide better UX and validation opportunities.

How do I use refs with dynamic lists?

Create an array of refs or use a Map. For lists, I usually create refs dynamically: const itemRefs = useRef([]) and assign them by index.

❤️ 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