2025-01-02 Web Development, Programming

Practical React UseRef Examples

By O. Wolfson

Example 1: Accessing DOM Elements

One of the most common uses of useRef is to interact directly with DOM elements.

Without useRef (Issue: Cannot Focus Input)

jsx
function InputWithoutRef() {
  const [inputValue, setInputValue] = useState("");

  const focusInput = () => {
    // Issue: No way to programmatically focus the input
    console.log("Cannot focus the input without a reference!");
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

export default InputWithoutRef;

Problem: There's no way to programmatically focus the input because React's state and props don't provide direct access to the DOM.


With useRef (Issue Resolved)

jsx

function InputWithRef() {
const inputRef = useRef(null); // Create a ref for the input element

const focusInput = () => {
// Use the ref to focus the input
if (inputRef.current) {
inputRef.current.focus();
}
};

return (

<div>
  <input ref={inputRef} type="text" />
  <button onClick={focusInput}>Focus Input</button>
</div>
); }

export default InputWithRef;

How It Fixes the Issue:

  • useRef provides a ref object (inputRef) that connects directly to the DOM element.
  • Calling inputRef.current.focus() programmatically focuses the input, solving the issue.

Example 2: Tracking Previous State

Another common use of useRef is to store the previous value of a state or prop without causing a re-render.

Without useRef (Issue: Cannot Track Previous State)

Current State: 0
Previous State: 0
jsx
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";

function CounterWithoutRef() {
  const [count, setCount] = useState(0);
  const [previousCount, setPreviousCount] = useState(count);

  useEffect(() => {
    setPreviousCount(count);
  }, [count]);

  return (
    <div className="flex flex-col items-center gap-4 justify-center p-10">
      <div className="flex flex-col items-center gap-4 justify-center p-10 border border-gray-200 rounded-md">
        <div className="text-2xl font-bold text-center flex flex-col gap-2">
          <div>Current State: {count}</div>
          <div>Previous State: {previousCount ?? "N/A"}</div>
        </div>
        <Button
          type="button"
          onClick={() => setCount(count + 1)}
          className="text-lg"
        >
          Increment
        </Button>
      </div>
    </div>
  );
}

export default CounterWithoutRef;

Problem: The previousCount variable resets on every render because it doesn’t persist across renders.

With useRef (Issue Resolved)

Current State: 0
Previous State: N/A
jsx
"use client";
import { useState, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button";

function CounterWithRef() {
  const [count, setCount] = useState(0);
  const previousCountRef = (useRef < number) | (null > null); // Initialize a ref to store the previous count
  useEffect(() => {
    previousCountRef.current = count; // Update the ref after every render
  }, [count]);

  return (
    <div className="flex flex-col items-center gap-4 justify-center p-10">
      <div className="flex flex-col items-center gap-4 justify-center p-10 border border-gray-200 rounded-md">
        <div className="text-2xl font-bold text-center flex flex-col gap-2">
          <div>Current State: {count}</div>
          <div>Previous State: {previousCountRef.current ?? "N/A"}</div>
        </div>
        <Button
          type="button"
          onClick={() => setCount(count + 1)}
          className="text-lg"
        >
          Increment
        </Button>
      </div>
    </div>
  );
}

export default CounterWithRef;

How It Fixes the Issue:

  • useRef persists the previous value across renders (previousCountRef.current).
  • The previousCount value correctly reflects the value before the latest render.

Example 3: Storing Mutable Values Across Renders

useRef is valuable for storing values that need to persist across renders without causing a re-render.

Example: Storing a Timeout ID

jsx
import { useState, useRef } from "react";

function TimerWithRef() {
  const [count, setCount] = useState(0);
  const timeoutIdRef = useRef(null); // Store the timeout ID

  const startTimer = () => {
    timeoutIdRef.current = setTimeout(() => {
      setCount((prev) => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearTimeout(timeoutIdRef.current); // Clears the active timeout reliably
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start Timer</button>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

export default TimerWithRef;

Why Useful:

  • useRef ensures the timeout ID is stored persistently across renders.
  • The stopTimer function reliably clears the timeout without causing re-renders.

Example 4: Stabilizing Function References

Another common use case for useRef is to prevent the re-creation of functions or objects between renders, improving performance.

Example: Stabilizing a Callback Function

jsx
import { useState, useRef, useEffect } from "react";

function MouseTrackerWithRef() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const handleMouseMoveRef = useRef();

  useEffect(() => {
    handleMouseMoveRef.current = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };
  });

  useEffect(() => {
    const mouseMoveHandler = (event) => handleMouseMoveRef.current(event);

    window.addEventListener("mousemove", mouseMoveHandler);

    return () => window.removeEventListener("mousemove", mouseMoveHandler);
  }, []);

  return (
    <p>
      Mouse position: {position.x}, {position.y}
    </p>
  );
}

export default MouseTrackerWithRef;

Why Useful:

  • useRef allows the event listener to reference a stable function, avoiding unnecessary cleanup and re-attachment.
  • This improves performance and prevents memory leaks.

Example 5: Combining useRef and Closures

useRef is also valuable when working with closures. By using useRef, you can avoid the problem of stale closures in asynchronous callbacks.

Without useRef (Issue: Stale Closure in setInterval)

jsx
import { useState, useEffect } from "react";

function TimerWithoutRef() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log("Count:", count); // Stale closure always sees initial count
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return <p>Count: {count}</p>;
}

export default TimerWithoutRef;

With useRef (Issue Resolved)

jsx
import { useState, useRef, useEffect } from "react";

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

  useEffect(() => {
    countRef.current = count; // Update the ref whenever count changes
  }, [count]);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log("Count:", countRef.current); // Access the latest count value
      setCount(countRef.current + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return <p>Count: {count}</p>;
}

export default TimerWithRef;

How It Fixes the Issue:

  • useRef provides access to the latest state value, avoiding the stale closure problem.
  • The setInterval callback always references the up-to-date countRef.current.

Conclusion

useRef is a versatile hook in React, solving problems ranging from DOM manipulation to managing stateful values and stabilizing references. By understanding its various use cases, you can write more efficient and predictable React components. Whether it’s interacting with the DOM, tracking previous state, managing mutable values, stabilizing functions, or avoiding stale closures, useRef is an indispensable tool for modern React development.