Tell us about the useRef hook, how it works, where it is used and what for?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#React #Hooks

Brief Answer

useRef is a hook in React that returns a mutable ref object which persists across component renders. The main property of this object is current, which can store any value.

Key features of useRef:

  • Changing .current doesn’t trigger a re-render
  • The value persists between renders
  • Each component instance gets its own independent ref

Main applications:

  1. Accessing DOM elements
  2. Storing previous state values
  3. Holding mutable values without triggering re-renders

Example of accessing a DOM element:

function TextInputWithFocusButton() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </>
  );
}

Full Answer

The useRef hook is one of React’s built-in hooks that provides a way to create a mutable value that doesn’t cause a re-render when updated. This makes it ideal for certain scenarios where useState would be overkill or inappropriate. 🔍

How Does useRef Work?

useRef returns a simple JavaScript object with a single property called current:

// Creating a ref with initial value null
const myRef = useRef(null);
console.log(myRef); // { current: null }
 
// You can set an initial value
const countRef = useRef(0);
console.log(countRef); // { current: 0 }
 
// Changing the value doesn't cause a re-render
countRef.current = countRef.current + 1;

Unlike state, changing current happens synchronously and doesn’t cause the component to re-render.

1. Accessing DOM Elements

The most common use of useRef is to get direct access to DOM nodes:

function AutoFocusInput() {
  const inputRef = useRef(null);
  
  // After the component mounts, focus the input
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

Controlling video and audio elements:

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  
  const handlePlay = () => {
    videoRef.current.play();
  };
  
  const handlePause = () => {
    videoRef.current.pause();
  };
  
  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}

Measuring element dimensions:

function MeasureExample() {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    if (divRef.current) {
      setDimensions({
        width: divRef.current.offsetWidth,
        height: divRef.current.offsetHeight
      });
    }
  }, []);
  
  return (
    <>
      <div ref={divRef} style={{ width: '100%', height: '100px', border: '1px solid black' }}>
        Element to measure
      </div>
      <p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
    </>
  );
}

2. Storing Previous State Values

Tracking previous state values that aren’t directly available:

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
  
  useEffect(() => {
    // Save current count value after render
    prevCountRef.current = count;
  });
  
  const prevCount = prevCountRef.current;
  
  return (
    <div>
      <h1>Now: {count}, before: {prevCount !== undefined ? prevCount : 'No previous value'}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

3. Storing Mutable Values

When you need to store a value that doesn’t affect the UI:

function IntervalExample() {
  const [count, setCount] = useState(0);
  const intervalIdRef = useRef(null);
  
  const startCounter = () => {
    if (intervalIdRef.current !== null) return;
    
    intervalIdRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stopCounter = () => {
    clearInterval(intervalIdRef.current);
    intervalIdRef.current = null;
  };
  
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalIdRef.current !== null) {
        clearInterval(intervalIdRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={startCounter}>Start</button>
      <button onClick={stopCounter}>Stop</button>
    </div>
  );
}

4. Caching Calculations Without Re-renders

Storing computation results that shouldn’t cause re-renders:

function ExpensiveComponent({ data }) {
  const cachedDataRef = useRef(null);
  
  if (cachedDataRef.current === null || cachedDataRef.current.originalData !== data) {
    // Perform expensive calculations only when data changes
    const processedResult = expensiveCalculation(data);
    
    cachedDataRef.current = {
      originalData: data,
      processedResult
    };
  }
  
  // Use the cached result
  return <div>{cachedDataRef.current.processedResult}</div>;
}

5. Creating Custom Hooks with useRef

Example of a hook to track if a component is mounted:

function useIsMounted() {
  const isMountedRef = useRef(false);
  
  useEffect(() => {
    isMountedRef.current = true;
    
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  
  return isMountedRef;
}
 
// Usage
function AsyncComponent() {
  const [data, setData] = useState(null);
  const isMountedRef = useIsMounted();
  
  useEffect(() => {
    fetchData().then(result => {
      // Check if component is still mounted before updating state
      if (isMountedRef.current) {
        setData(result);
      }
    });
  }, []);
  
  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Comparison With Other Data Storage Methods

useRef vs useState

// With useState - re-renders on every change
function CounterWithState() {
  const [count, setCount] = useState(0);
  
  // Re-renders the component
  const increment = () => setCount(count + 1);
  
  console.log("Rendering component with useState");
  
  return (
    <div>
      <p>Counter (useState): {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
 
// With useRef - no re-renders
function CounterWithRef() {
  const countRef = useRef(0);
  const [, forceUpdate] = useState({});
  
  // Doesn't trigger a re-render
  const increment = () => {
    countRef.current += 1;
  };
  
  // To display the current value we need to force a re-render
  const incrementAndUpdate = () => {
    increment();
    forceUpdate({});
  };
  
  console.log("Rendering component with useRef");
  
  return (
    <div>
      <p>Counter (useRef): {countRef.current}</p>
      <button onClick={increment}>Increment (no UI update)</button>
      <button onClick={incrementAndUpdate}>Increment and update UI</button>
    </div>
  );
}

useRef vs createRef

function CompareRefs() {
  // Created anew on each render
  const createRefExample = React.createRef();
  
  // Persists between renders
  const useRefExample = useRef();
  
  // Demonstrating differences
  const [, forceRender] = useState({});
  
  useEffect(() => {
    console.log("After mounting:");
    console.log("createRef current value:", createRefExample.current);
    console.log("useRef current value:", useRefExample.current);
    
    // Set values
    createRefExample.current = "createRef value";
    useRefExample.current = "useRef value";
    
    console.log("After setting:");
    console.log("createRef value:", createRefExample.current);
    console.log("useRef value:", useRefExample.current);
  }, []);
  
  const handleClick = () => {
    // Re-render the component
    forceRender({});
    
    // After re-render
    console.log("After re-render:");
    console.log("createRef value:", createRefExample.current); // null
    console.log("useRef value:", useRefExample.current); // "useRef value"
  };
  
  return <button onClick={handleClick}>Re-render</button>;
}

Common Mistakes

1. Trying to Observe Ref Changes

// ❌ Incorrect: useEffect doesn't track ref.current changes
function WrongWayToWatchRef() {
  const countRef = useRef(0);
  
  useEffect(() => {
    console.log("Value changed:", countRef.current);
  }, [countRef.current]); // This won't work as expected
  
  return (
    <button onClick={() => { countRef.current += 1; }}>
      Increment (won't trigger useEffect)
    </button>
  );
}
 
// ✅ Correct: use state for values you want to track
function CorrectWayToWatchChanges() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log("Value changed:", count);
  }, [count]);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Increment (will trigger useEffect)
    </button>
  );
}

2. Initializing Refs Conditionally

// ❌ Incorrect: creating ref inside a condition
function ConditionalRef({ shouldRender }) {
  let inputRef;
  
  if (shouldRender) {
    inputRef = useRef(null);
  }
  
  // This will cause errors when shouldRender changes
  return shouldRender ? <input ref={inputRef} /> : null;
}
 
// ✅ Correct: always create refs at the top level
function CorrectConditionalRendering({ shouldRender }) {
  const inputRef = useRef(null);
  
  return shouldRender ? <input ref={inputRef} /> : null;
}

3. Trying to Use Ref Before Assignment

// ❌ Incorrect: accessing ref before it's assigned
function TooEarlyAccess() {
  const inputRef = useRef(null);
  
  // This code runs before the ref is assigned
  console.log(inputRef.current.value); // Error: Cannot read property 'value' of null
  
  return <input ref={inputRef} defaultValue="Initial value" />;
}
 
// ✅ Correct: use useEffect to access after render
function CorrectAccess() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // This code runs after the DOM is ready
    console.log(inputRef.current.value);
  }, []);
  
  return <input ref={inputRef} defaultValue="Initial value" />;
}

Summary

useRef in React is used for:

  • Accessing DOM elements and their methods (focus(), play(), etc.)
  • Storing previous state values
  • Preserving values between renders without causing re-renders
  • Storing timers, intervals, and other references not related to UI
  • Creating custom hooks to track component state

Key features:

  • Changing .current doesn’t trigger a re-render
  • The value persists throughout the component lifecycle
  • Differs from useState by not causing automatic re-renders
  • Differs from createRef by preserving value between renders

Understanding useRef and applying it correctly is an important part of optimizing performance and interacting with the DOM in React applications. 🚀


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site, and improve yourself every day 💪