Using useRef to store values but expecting re-renders

Loading

A common misconception in React is expecting components to re-render when values stored in useRef change. This misunderstanding can lead to bugs where UI doesn’t update as expected.

The Core Behavior of useRef

useRef creates a mutable object that persists across renders, but changing it does not trigger re-renders:

function Counter() {
  const countRef = useRef(0); // { current: 0 }

  const increment = () => {
    countRef.current += 1; // ❌ No re-render
    console.log(countRef.current); // Value updates but UI stays same
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <p>Count: {countRef.current}</p> {/* Won't update! */}
    </div>
  );
}

Why useRef Doesn’t Cause Re-renders

  1. Performance Optimization: Refs are designed for mutable values that shouldn’t affect rendering
  2. Explicit Updates: React only re-renders in response to state/props changes
  3. Escape Hatch: Refs provide a way to store values outside React’s reactivity system

When to Use useRef vs useState

FeatureuseRefuseState
Triggers re-render❌ No✅ Yes
Value persistence✅ Between renders✅ Between renders
Use caseDOM references, mutable values that shouldn’t trigger updatesData that should drive UI updates

Correct Patterns

1. Using useRef for Non-UI Values

function Timer() {
  const intervalRef = useRef(null); // Good for timer IDs

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  // ... No need to display intervalRef in UI
}

2. Combining with useState for Updates

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  const increment = () => {
    const newCount = countRef.current + 1;
    countRef.current = newCount;
    setCount(newCount); // ✅ Triggers re-render
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <p>Count: {count}</p> {/* Updates correctly */}
    </div>
  );
}

Common Mistakes and Fixes

❌ Expecting UI Updates from Ref Changes

function UserProfile() {
  const userRef = useRef({ name: 'Alice' });

  const updateName = () => {
    userRef.current.name = 'Bob'; // ❌ No re-render
  };

  return (
    <div>
      <p>{userRef.current.name}</p> {/* Stays 'Alice' */}
      <button onClick={updateName}>Update</button>
    </div>
  );
}

Fix: Use state for UI-bound values

function UserProfile() {
  const [user, setUser] = useState({ name: 'Alice' });

  const updateName = () => {
    setUser({ ...user, name: 'Bob' }); // ✅ Triggers re-render
  };

  return (
    <div>
      <p>{user.name}</p> {/* Updates to 'Bob' */}
      <button onClick={updateName}>Update</button>
    </div>
  );
}

Advanced Patterns

1. Accessing Previous State

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count; // Update ref after render
  });

  return (
    <div>
      <p>Current: {count}, Previous: {prevCountRef.current}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

2. Imperative Handle Pattern

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    value: () => inputRef.current.value
  }));

  return <input ref={inputRef} />;
}

Debugging Tips

  1. Log ref values in useEffect to verify updates:
   useEffect(() => {
     console.log('Ref value:', myRef.current);
   });
  1. Use React DevTools to inspect ref values
  2. Verify whether you really need UI updates (use state) or just value persistence (use ref)

Key Takeaways

  1. useRef changes don’t trigger re-renders – use useState if you need UI updates
  2. Refs are perfect for:
  • Storing mutable values that shouldn’t affect rendering
  • DOM node references
  • Previous value tracking
  1. Combine refs with state when you need both persistence and reactivity
  2. Always access ref values via the .current property

Understanding this distinction helps prevent bugs where your component’s UI doesn’t update as expected while properly leveraging refs for their intended purposes.

Leave a Reply

Your email address will not be published. Required fields are marked *