Introduction to useRef

What is useRef?

useRef is one of React's built-in hooks that provides a way to access and interact with DOM elements directly or to persist values across renders without causing re-renders. Think of it as a "box" that can hold a mutable value in its .current property that persists for the full lifetime of the component.

Why Do We Need useRef?

Without useRef, it would be difficult to:

  • Access DOM elements directly to call imperative methods (like focus() or play())
  • Store values that need to persist between renders without triggering re-renders
  • Keep track of previous state or prop values
  • Store mutable values that don't affect the UI

Regular variables are reset on each render, and state variables trigger re-renders when changed, but useRef provides a way to store persistent values that don't cause re-renders when updated.

useRef Made Simple

Imagine you're taking notes in a class:

  1. You write notes in your notebook (the ref) throughout the class
  2. You can update these notes at any time (modifying the .current property)
  3. Updating your notes doesn't interrupt the class (no re-renders)
  4. Your notebook stays with you for the entire class (persists between renders)
  5. At certain points, you might refer to your notes to help you understand something (accessing the ref value)

This is exactly how useRef works in React!

Basic Syntax

import { useRef } from "react";

function TextInput() {
  // Create a ref with an initial value of null
  const inputRef = useRef(null);

  // Function to focus the input element
  const focusInput = () => {
    // Access the DOM element through the .current property
    inputRef.current.focus();
  };

  return (
    <div>
      {/* Attach the ref to a DOM element */}
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

Let's break this down:

  1. We import the useRef hook from React
  2. We call useRef(null) to create a ref object, with null as the initial value
  3. The ref object has a mutable .current property, which initially points to null
  4. We attach the ref to the input element using the ref attribute
  5. After rendering, inputRef.current will point to the actual DOM element
  6. We can then call DOM methods on this element, like focus()

Common Uses of useRef

Accessing DOM Elements

The most common use case is to access and manipulate DOM elements:

function VideoPlayer() {
  const videoRef = useRef(null);

  const playVideo = () => {
    videoRef.current.play();
  };

  const pauseVideo = () => {
    videoRef.current.pause();
  };

  return (
    <div>
      <video ref={videoRef} src="/video.mp4" />
      <button onClick={playVideo}>Play</button>
      <button onClick={pauseVideo}>Pause</button>
    </div>
  );
}

Persisting Values Between Renders

Refs can store values that persist across renders without triggering re-renders:

function RenderCounter() {
  // This state update will cause a re-render
  const [count, setCount] = useState(0);

  // This ref tracks render count but doesn't cause re-renders when updated
  const renderCount = useRef(0);

  // Update the ref on each render
  useEffect(() => {
    renderCount.current += 1;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <p>This component has rendered {renderCount.current} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

Storing Previous Values

Refs can store previous values of props or state for comparison:

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

  useEffect(() => {
    // Store current count value in ref after render
    prevCountRef.current = count;
  }, [count]);

  // Get previous count value (undefined on first render)
  const prevCount = prevCountRef.current;

  return (
    <div>
      <p>
        Now: {count}, Before: {prevCount !== undefined ? prevCount : "N/A"}
      </p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Creating Instance Variables

Refs can be used to create instance-like variables in function components:

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  // Use ref for values that shouldn't trigger re-renders
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (!isRunning) {
      setIsRunning(true);
      intervalRef.current = setInterval(() => {
        setTime((t) => t + 1);
      }, 1000);
    }
  };

  const stopTimer = () => {
    if (isRunning) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
      setIsRunning(false);
    }
  };

  // Clean up the interval on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div>
      <p>Time: {time} seconds</p>
      <button onClick={startTimer} disabled={isRunning}>
        Start
      </button>
      <button onClick={stopTimer} disabled={!isRunning}>
        Stop
      </button>
    </div>
  );
}

useRef with TypeScript

In TypeScript, you should specify the type of the ref's current value:

// For DOM elements
const inputRef = useRef<HTMLInputElement>(null);

// For custom values
const countRef = useRef<number>(0);

When working with DOM refs, there are two patterns to be aware of:

  1. Read-only ref: If you only need to read from the DOM element and don't need to initialize it with a value:
// The null assertion (!) is not needed when accessing current
const inputRef = useRef<HTMLInputElement>(null);

// Checking if ref is assigned before using it
const focusInput = () => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
};
  1. Mutable ref: If you need to both initialize the ref and modify its current value:
// The type has to allow for null
const countRef = useRef<number | null>(null);

// Initialize in useEffect or event handlers
useEffect(() => {
  countRef.current = 0;
}, []);

useRef vs. useState

It's important to understand when to use useRef versus useState:

// Use useState when:
// 1. The value should be displayed in the UI
// 2. Changes to the value should trigger re-renders
const [count, setCount] = useState(0);

// Use useRef when:
// 1. The value shouldn't trigger re-renders when changed
// 2. You need to access DOM elements
// 3. You need to persist values between renders
const countRef = useRef(0);

Key differences:

  • Updating state with setCount causes a re-render; updating countRef.current does not
  • count is immutable within a render; countRef.current can be changed anytime
  • State updates are queued and batched; ref updates happen immediately

useRef in Real Life

Here's a practical example of a form component that uses refs in several ways:

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

function AdvancedForm() {
  // State for form values
  const [formValues, setFormValues] = useState({
    name: "",
    email: "",
  });

  // State for form submission status
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSubmitted, setIsSubmitted] = useState(false);

  // Refs for DOM elements
  const nameInputRef = useRef<HTMLInputElement>(null);
  const formRef = useRef<HTMLFormElement>(null);

  // Ref for tracking previous email value
  const previousEmailRef = useRef("");

  // Ref for tracking submission attempts
  const submissionAttemptsRef = useRef(0);

  // Focus the name input on initial render
  useEffect(() => {
    if (nameInputRef.current) {
      nameInputRef.current.focus();
    }
  }, []);

  // Store previous email value after each render
  useEffect(() => {
    previousEmailRef.current = formValues.email;
  }, [formValues.email]);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormValues((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);

    // Increment submission attempts
    submissionAttemptsRef.current += 1;

    try {
      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 1500));

      // Log values and refs
      console.log("Form Values:", formValues);
      console.log("Previous Email:", previousEmailRef.current);
      console.log("Submission Attempts:", submissionAttemptsRef.current);

      setIsSubmitted(true);
    } catch (error) {
      console.error("Submission failed:", error);
    } finally {
      setIsSubmitting(false);
    }
  };

  const resetForm = () => {
    // Reset form values
    setFormValues({ name: "", email: "" });
    setIsSubmitted(false);

    // Reset form using the ref
    if (formRef.current) {
      formRef.current.reset();
    }

    // Focus back on the name input
    if (nameInputRef.current) {
      nameInputRef.current.focus();
    }
  };

  if (isSubmitted) {
    return (
      <div>
        <h2>Form Submitted Successfully!</h2>
        <p>Thank you for your submission, {formValues.name}.</p>
        <button onClick={resetForm}>Submit Another Response</button>
      </div>
    );
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          ref={nameInputRef}
          name="name"
          value={formValues.name}
          onChange={handleChange}
          required
        />
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formValues.email}
          onChange={handleChange}
          required
        />
        {previousEmailRef.current !== formValues.email && formValues.email && (
          <small>Email changed from {previousEmailRef.current || "(empty)"}</small>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Submitting..." : "Submit"}
      </button>

      {submissionAttemptsRef.current > 0 && (
        <p>You've attempted to submit {submissionAttemptsRef.current} times.</p>
      )}
    </form>
  );
}

This example demonstrates:

  1. Using refs to access DOM elements (nameInputRef, formRef)
  2. Using a ref to track previous values (previousEmailRef)
  3. Using a ref to store data that shouldn't trigger re-renders (submissionAttemptsRef)
  4. Working with the DOM imperatively using refs

Common Patterns and Best Practices

Initializing Refs After Render

const canvasRef = useRef(null);

useEffect(() => {
  if (canvasRef.current) {
    const context = canvasRef.current.getContext("2d");
    // Use the canvas context for drawing
  }
}, []);

return <canvas ref={canvasRef} width={500} height={300} />;

Callback Refs for Dynamic Refs

Sometimes you need more control over when and how a ref is attached:

function MeasureExample() {
  const [height, setHeight] = useState(0);

  // Callback ref that measures the element
  const measuredRef = useCallback((node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <p>The above header is {Math.round(height)}px tall</p>
    </>
  );
}

Avoiding Common Mistakes

  1. Don't use refs for values that should trigger re-renders:
// Wrong: UI won't update when count changes
const countRef = useRef(0);
const increment = () => {
  countRef.current += 1;
};

// Right: UI updates when count changes
const [count, setCount] = useState(0);
const increment = () => {
  setCount((c) => c + 1);
};
  1. Don't read or write refs during rendering:
// Wrong: Reading refs during rendering can cause inconsistencies
function Component() {
  const ref = useRef(0);

  // Don't do this during rendering
  ref.current += 1;

  return <div>{ref.current}</div>;
}

// Right: Update refs in event handlers or effects
function Component() {
  const ref = useRef(0);
  const [, forceUpdate] = useState({});

  const handleClick = () => {
    ref.current += 1;
    forceUpdate({});
  };

  return (
    <div>
      <p>Count: {ref.current}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

Exercises

Exercise 1: Auto-Focus Input Field

Objective: Create a component with a button that, when clicked, displays an input field and automatically focuses on it.

Detailed Instructions:

  1. Create a new file called auto-focus-input.tsx in your components folder.
  2. Import React, useState, useRef, and useEffect.
  3. Create a functional component named AutoFocusInput.
  4. Inside your component:
    • Create a state variable isVisible to control the input's visibility
    • Create a ref for the input element
    • Add a button that toggles the input's visibility
    • When the input becomes visible, it should automatically get focus
  5. Use the useEffect hook to focus the input when it becomes visible.
  6. Return JSX that includes the button and conditionally renders the input based on the isVisible state.

Starter Code:

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

const AutoFocusInput = () => {
  // Add your state for visibility

  // Add your ref for the input element

  // Add your effect for auto-focusing

  // Add your toggle function

  return <div>{/* Add your button and conditional rendering for the input */}</div>;
};

export default AutoFocusInput;

Exercise 2: Previous Value Tracker

Objective: Create a component that displays both the current and previous values of a counter.

Detailed Instructions:

  1. Create a new file called previous-value-tracker.tsx in your components folder.
  2. Import React, useState, useRef, and useEffect.
  3. Create a functional component named PreviousValueTracker.
  4. Inside your component:
    • Create a state variable count for the counter
    • Create a ref previousCountRef to store the previous count value
    • Use an effect to update the ref after each render
    • Add buttons to increment and decrement the counter
  5. Display both the current and previous values in your component.
  6. The previous value should be "None" on the first render.

Starter Code:

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

const PreviousValueTracker = () => {
  // Add your state for the counter

  // Add your ref for the previous value

  // Add your effect to update the previous value

  return (
    <div>
      {/* Display the current and previous values */}
      {/* Add buttons to update the counter */}
    </div>
  );
};

export default PreviousValueTracker;

Exercise 3: Click Outside Detector

Objective: Create a dropdown component that closes when the user clicks outside of it, using refs to detect outside clicks.

Detailed Instructions:

  1. Create a new file called dropdown-with-click-outside.tsx in your components folder.
  2. Import React, useState, useRef, and useEffect.
  3. Create a functional component named DropdownWithClickOutside.
  4. Inside your component:
    • Create a state variable isOpen to track if the dropdown is open
    • Create a ref for the dropdown container element
    • Add an effect that adds a click event listener to the document
    • The event listener should check if the click occurred outside the dropdown
    • If the click was outside, close the dropdown
  5. Remember to clean up the event listener when the component unmounts.
  6. Include a button to toggle the dropdown and a dropdown menu with some items.

Starter Code:

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

const DropdownWithClickOutside = () => {
  // Add your state for the dropdown open/closed state

  // Add your ref for the dropdown container

  // Add your effect for the click outside detection

  return <div>{/* Add the dropdown toggle button and menu */}</div>;
};

export default DropdownWithClickOutside;

Remember

  • Use refs to access DOM elements when you need to call imperative methods
  • Use refs to store values that shouldn't trigger re-renders when they change
  • Don't read or update refs during rendering, do it in effects or event handlers
  • Refs are mutable and persist across renders
  • Clean up any event listeners or subscriptions created with refs
  • With TypeScript, always specify the correct type for your refs

Now that you understand useRef, you can work with DOM elements directly and efficiently maintain values throughout a component's lifecycle without unnecessary re-renders!