Skip to main content

Command Palette

Search for a command to run...

useRef Explained: Handling DOM Elements in React

Learn to Access and Manage DOM Elements in React Using useRef

Updated
5 min read
useRef Explained: Handling DOM Elements in React

Introduction

In React's declarative world, we typically let React handle DOM updates. However, there are times when you need to directly interact with DOM elements—focusing an input field, measuring an element's dimensions, or integrating with third-party libraries. This is where useRef becomes essential.

In this guide, we'll explore how to use useRef to access DOM nodes, starting from the basics and building up to production-ready patterns.

What is useRef?

useRef is a React Hook that creates a mutable reference object that persists across re-renders. Unlike state, updating a ref doesn't trigger a component re-render, making it perfect for storing values that shouldn't affect the visual output.

Basic Syntax

const myRef = useRef(initialValue);

The ref object has a single property called current that holds the actual value. When you attach a ref to a DOM element, React automatically sets ref.current to that DOM node.

Your First Ref: Focusing an Input

Let's start with the most common use case—focusing an input field when a component mounts.

Basic Example

import { useRef, useEffect } from 'react';

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

  useEffect(() => {
    // Focus the input when component mounts
    inputRef.current.focus();
  }, []);

  return (
    <input 
      ref={inputRef}
      type="text" 
      placeholder="Start typing..." 
    />
  );
}

How it works:

  1. We create a ref with useRef(null) (null is the initial value)

  2. We attach the ref to the input using the ref attribute

  3. React sets inputRef.current to the actual DOM element

  4. In useEffect, we call inputRef.current.focus() to focus the input

Production-Grade Example: Advanced Search Component

Here's a real-world example that demonstrates multiple ref patterns you'll use in production applications.

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

function AdvancedSearchBar({ onSearch, autoFocus = false }) {
  const [query, setQuery] = useState('');
  const [isSearching, setIsSearching] = useState(false);
  const inputRef = useRef(null);
  const searchTimeoutRef = useRef(null);

  // Auto-focus on mount if specified
  useEffect(() => {
    if (autoFocus && inputRef.current) {
      inputRef.current.focus();
    }
  }, [autoFocus]);

  // Cleanup timeout on unmount
  useEffect(() => {
    return () => {
      if (searchTimeoutRef.current) {
        clearTimeout(searchTimeoutRef.current);
      }
    };
  }, []);

  const handleInputChange = (e) => {
    const value = e.target.value;
    setQuery(value);

    // Clear existing timeout
    if (searchTimeoutRef.current) {
      clearTimeout(searchTimeoutRef.current);
    }

    // Debounce search
    setIsSearching(true);
    searchTimeoutRef.current = setTimeout(() => {
      onSearch(value);
      setIsSearching(false);
    }, 500);
  };

  const handleClear = () => {
    setQuery('');
    onSearch('');
    // Focus input after clearing
    inputRef.current?.focus();
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') {
      handleClear();
    }
  };

  return (
    <div className="search-container">
      <input
        ref={inputRef}
        type="text"
        value={query}
        onChange={handleInputChange}
        onKeyDown={handleKeyDown}
        placeholder="Search..."
        aria-label="Search input"
        aria-busy={isSearching}
      />
      {query && (
        <button 
          onClick={handleClear}
          aria-label="Clear search"
          type="button"
        >
          Clear
        </button>
      )}
      {isSearching && <span>Searching...</span>}
    </div>
  );
}

Key Patterns Demonstrated

1. Conditional Focus

if (autoFocus && inputRef.current) {
  inputRef.current.focus();
}

Always check if ref.current exists before accessing it. This prevents errors during initial render or if the component unmounts.

2. Multiple Refs We use two refs: one for the DOM node (inputRef) and one for storing the timeout ID (searchTimeoutRef). Refs aren't just for DOM nodes—they're perfect for any mutable value that shouldn't cause re-renders.

3. Optional Chaining

inputRef.current?.focus();

Using ?. ensures we don't throw an error if the ref is null.

Common DOM Operations with useRef

Measuring Elements

function ImageGallery() {
  const containerRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (containerRef.current) {
      const { width, height } = containerRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div ref={containerRef}>
      <p>Container size: {dimensions.width}x{dimensions.height}</p>
    </div>
  );
}

Scrolling to Elements

function ChatWindow({ messages }) {
  const bottomRef = useRef(null);

  useEffect(() => {
    // Scroll to bottom when new messages arrive
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div className="chat-messages">
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
      <div ref={bottomRef} />
    </div>
  );
}

Playing/Pausing Media

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const togglePlay = () => {
    if (videoRef.current) {
      if (isPlaying) {
        videoRef.current.pause();
      } else {
        videoRef.current.play();
      }
      setIsPlaying(!isPlaying);
    }
  };

  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={togglePlay}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
    </div>
  );
}

Best Practices

1. Always Check for Null

// Good
if (inputRef.current) {
  inputRef.current.focus();
}

// Better
inputRef.current?.focus();

// Bad - can throw error
inputRef.current.focus();

2. Don't Overuse Refs

Use refs only when you need to step outside React's declarative paradigm. For most UI updates, state and props are the right choice.

// Bad - using ref for something state should handle
const countRef = useRef(0);
countRef.current += 1; // Won't trigger re-render

// Good - use state for values that affect rendering
const [count, setCount] = useState(0);
setCount(c => c + 1); // Triggers re-render

3. Clean Up Side Effects

When using refs with timers, event listeners, or subscriptions, always clean them up.

useEffect(() => {
  const element = elementRef.current;

  const handleClick = () => console.log('clicked');
  element?.addEventListener('click', handleClick);

  return () => {
    element?.removeEventListener('click', handleClick);
  };
}, []);

4. Use TypeScript for Type Safety

const inputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const divRef = useRef<HTMLDivElement>(null);

Common Pitfalls to Avoid

Pitfall 1: Accessing Refs During Render

// Bad - don't access ref.current during render
function MyComponent() {
  const ref = useRef(null);
  console.log(ref.current); // Will be null on first render
  return <div ref={ref}>Content</div>;
}

// Good - access in useEffect or event handlers
function MyComponent() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref.current); // Now it's safe
  }, []);

  return <div ref={ref}>Content</div>;
}

Pitfall 2: Forgetting Dependencies

// Bad - missing dependency
useEffect(() => {
  inputRef.current?.focus();
}, []); // If inputRef comes from props, this is wrong

// Good - include all dependencies
useEffect(() => {
  inputRef.current?.focus();
}, [inputRef]);

Conclusion

The useRef hook is a powerful tool for accessing DOM nodes in React. Remember these key points:

  • Use useRef when you need direct DOM access

  • Always check if ref.current exists before using it

  • Access refs in useEffect or event handlers, not during render

  • Clean up any side effects that use refs

  • Prefer state for values that should trigger re-renders

With these patterns, you're ready to handle DOM manipulation confidently in your React applications.


Further Reading: