Using useRef to store values but expecting re-renders

Loading

Using useRef Without Triggering Re-renders

A common React misunderstanding occurs when developers use useRef to store values but expect the component to re-render when the ref’s value changes. This doesn’t happen because refs are designed to be mutable storage that doesn’t trigger reactivity.

The Problem

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

  const increment = () => {
    countRef.current++; // ❌ Mutation won't trigger re-render
    console.log(countRef.current); // Value changes but UI doesn't update
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <span>Count: {countRef.current}</span> {/* Stays at 0 */}
    </div>
  );
}

Why this happens:

  1. useRef provides a mutable object ({ current: value })
  2. Mutating current doesn’t trigger component re-renders
  3. Refs are designed for storing values without reactivity
  4. The component only renders when state/props change

Correct Solutions

1. Use State for Reactive Values (Recommended)

function Counter() {
  const [count, setCount] = useState(0); // ✅ Triggers re-renders

  const increment = () => {
    setCount(c => c + 1); // Updates UI
  };

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

2. Combine Ref and State

function Counter() {
  const [_, forceUpdate] = useState(); // For re-renders
  const countRef = useRef(0);

  const increment = () => {
    countRef.current++;
    forceUpdate({}); // ✅ Force update when needed
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <span>Count: {countRef.current}</span>
    </div>
  );
}

3. Proper useRef Use Cases

function Form() {
  const inputRef = useRef(null); // ✅ Good for DOM references
  const renderCount = useRef(0); // ✅ Good for tracking values

  useEffect(() => {
    renderCount.current++; // Doesn't need re-render
    inputRef.current.focus(); // DOM operation
  }, []);

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

When to Use Refs vs State

useRefuseState
DOM element referencesValues that affect rendering
Persistent values between rendersReactive data
Storing previous valuesForm inputs
Interval/Timeout IDsAny visual state
Mutable variables that shouldn’t trigger updatesData that affects UI

Common Mistakes

  1. Expecting re-renders from ref changes:
   const valueRef = useRef(0);
   valueRef.current = 42; // ❌ No re-render
  1. Using refs for derived state:
   const filteredRef = useRef(items.filter(/*...*/)); // ❌ Use useMemo
  1. Overusing forceUpdate patterns:
   // ❌ Anti-pattern in most cases
   const [_, forceUpdate] = useState();

Best Practices

  1. Use state for any data that should trigger UI updates
  2. Use refs for:
  • Accessing DOM elements
  • Storing mutable values that shouldn’t affect rendering
  • Keeping previous values
  • Storing timeout/interval IDs
  1. Avoid forceUpdate – Rethink your component structure instead
  2. Combine with useEffect for side effects:
   const prevCountRef = useRef();
   useEffect(() => {
     prevCountRef.current = count; // Track previous value
   });

Advanced Patterns

1. Previous Value Tracking

function Counter({ count }) {
  const prevCountRef = useRef();

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

  const prevCount = prevCountRef.current;
  return <div>Now: {count}, Before: {prevCount}</div>;
}

2. Stable Function References

function Form() {
  const inputRef = useRef(null);

  // ✅ Stable function reference
  const focusInput = useCallback(() => {
    inputRef.current?.focus();
  }, []);

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

3. Measuring DOM Elements

function Box() {
  const divRef = useRef(null);
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    if (divRef.current) {
      setWidth(divRef.current.offsetWidth);
    }
  }, []);

  return <div ref={divRef}>Width: {width}px</div>;
}

Leave a Reply

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