Using useRef to store values but expecting re-renders

Loading

Understanding useRef and Component Re-renders

A common misconception in React is expecting a component to re-render when a useRef value changes. This doesn’t happen because refs are designed specifically to persist values without triggering re-renders.

The Core Issue

function Counter() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1;
    // ❌ Expecting component to re-render automatically
    console.log(countRef.current); // Value updates but no re-render
  };

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

Why Refs Don’t Trigger Re-renders

  1. Mutable Container: useRef returns a mutable object ({ current: value })
  2. No React Tracking: Changes to .current aren’t observed by React
  3. Performance Purpose: Designed for values that shouldn’t trigger updates
  4. Render Isolation: Values persist across renders without affecting output

When to Use Refs (Properly)

1. Storing Mutable Values Without Re-renders

function Timer() {
  const intervalRef = useRef();

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

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

2. Accessing DOM Elements

function InputFocus() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

3. Keeping Previous Values

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

  useEffect(() => {
    prevCountRef.current = count;
  });

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

Solutions When You Need Re-renders

1. Combine with useState (Recommended)

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>
      <div>Count: {count}</div>
    </div>
  );
}

2. Force Update Pattern (Rare Cases)

function useForceUpdate() {
  const [, setTick] = useState(0);
  return () => setTick(t => t + 1);
}

function Counter() {
  const countRef = useRef(0);
  const forceUpdate = useForceUpdate();

  const increment = () => {
    countRef.current += 1;
    forceUpdate(); // ✅ Manual re-render trigger
  };

  return /* ... */;
}

Key Differences: useRef vs useState

FeatureuseRefuseState
Triggers re-render❌ No✅ Yes
Value persistence✅ Across renders✅ Across renders
Best forDOM refs, intervalsUI state
UpdatesSynchronousAsynchronous
Read value.current propertyState variable

Common Pitfalls

  1. Expecting UI Updates from ref changes
  2. Overusing refs for state that should trigger UI
  3. Race Conditions when mixing refs and state
  4. Stale Closures in callbacks using ref values

Best Practices

  1. Use refs for values that shouldn’t affect rendering
  2. Combine with state when you need both persistence and updates
  3. Avoid derived state in refs – use useMemo instead
  4. Document ref usage since they’re less visible in the component
  5. Consider alternatives like context or state management for complex cases

Remember: Refs are escape hatches from React’s reactivity system. Use them when you need to store values that shouldn’t trigger re-renders, and combine them with state when you need both persistence and UI updates.

Leave a Reply

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