React useRef Complete Guide: DOM Manipulation and Performance Optimization

by Opeyemi Stephen12 min read
React useRef Complete Guide: DOM Manipulation and Performance Optimization
ReactJavaScriptTutorialAdvanced

Mastering useRef in React: A Deep Dive into Its Practical Uses#

React's useRef is one of those hooks that developers often struggle to understand at first. Unlike useState and useEffect, it doesn't seem as intuitive. Why do we need a hook that doesn't trigger re-renders? What problems does it solve in real-world applications?

This article provides an in-depth look at useRef, explaining why it exists, how it works, and when to use it effectively. By the end, you'll not only understand useRef but also know how to leverage it in practical applications to improve your React development workflow.

Understanding useRef at Its Core#

What Is useRef?#

useRef is a React Hook that allows you to persist values across renders without causing a component to re-render.

  • It stores a mutable reference to a value or DOM element
  • Unlike useState, updating useRef.current does not trigger a re-render
  • It is commonly used for DOM manipulation, storing previous values, and managing intervals/timeouts

How Does useRef Work?#

When you create a useRef, it returns an object with a .current property:

hljs jsx
const myRef = useRef(initialValue);
console.log(myRef.current); // Logs the initial value
  • myRef is an object that remains the same across renders
  • myRef.current holds the actual value, which can be changed without re-rendering

This is different from useState, where any update causes a component to re-render.

useRef vs. useState: When to Use Which?#

FeatureuseStateuseRef
Triggers Re-renders?YesNo
Persists Across Renders?YesYes
Ideal for UI Updates?YesNo
Used for DOM Manipulation?NoYes

When to Use useRef Instead of useState#

  1. DOM Manipulation – When working with elements directly (e.g., focusing an input field)
  2. Storing Values Without Re-renders – When keeping values that don't need to trigger a UI update
  3. Storing Previous Values – When comparing the previous state with the current state
  4. Managing Timers/Intervals – When setting timeouts or intervals without causing unnecessary re-renders

Practical Applications of useRef#

Let's explore real-world examples where useRef makes React applications more efficient.

1. Auto-Focusing an Input Field#

Imagine you have a login form, and you want the input field to be focused as soon as the page loads.

hljs jsx
import React, { useRef, useEffect } from "react";

function LoginForm() {
  const inputRef = useRef(null); // Step 1: Create the ref

  useEffect(() => {
    inputRef.current?.focus(); // Step 2: Automatically focus input on mount
  }, []);

  return (
    <input ref={inputRef} type="text" placeholder="Enter your username" />
  );
}

export default LoginForm;

Why does useRef work well here?

  • useRef stores the reference to the DOM element
  • useEffect runs once when the component mounts, focusing the input
  • Since useRef doesn't trigger re-renders, performance is not affected

2. Storing Previous State Values#

Sometimes, you want to keep track of the previous state value, such as tracking the last entered name.

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

function NameTracker() {
  const [name, setName] = useState("");
  const prevNameRef = useRef("");

  useEffect(() => {
    prevNameRef.current = name; // Store previous name
  }, [name]);

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Current: {name}</p>
      <p>Previous: {prevNameRef.current}</p>
    </div>
  );
}

export default NameTracker;

Why use useRef instead of useState?

  • useState updates trigger re-renders, while useRef doesn't
  • The previous name is stored without causing extra renders

3. Preventing Unnecessary Re-renders in a Timer#

Here's a stopwatch that updates every second without causing the entire component to re-render.

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

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null); // Store the interval ID

  const startTimer = () => {
    if (!intervalRef.current) {
      intervalRef.current = setInterval(() => {
        setSeconds((prev) => prev + 1);
      }, 1000);
    }
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <h1>Time: {seconds}s</h1>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

export default Stopwatch;

Why is useRef useful here?

  • It stores the interval ID without re-rendering the component
  • Prevents unnecessary state updates

Advanced useRef Patterns#

1. Measuring DOM Elements#

hljs jsx
import React, { useRef, useState, useEffect } from 'react';

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

  useEffect(() => {
    const updateDimensions = () => {
      if (elementRef.current) {
        const { width, height } = elementRef.current.getBoundingClientRect();
        setDimensions({ width, height });
      }
    };

    updateDimensions();
    window.addEventListener('resize', updateDimensions);
    
    return () => window.removeEventListener('resize', updateDimensions);
  }, []);

  return (
    <div ref={elementRef} style={{ padding: '20px', border: '1px solid #ccc' }}>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}
hljs jsx
import React, { useState, useRef, useCallback } from 'react';

function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');
  const timeoutRef = useRef(null);

  const debouncedSearch = useCallback((searchQuery) => {
    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
      onSearch(searchQuery);
    }, 300);
  }, [onSearch]);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  };

  return (
    <input
      type="text"
      value={query}
      onChange={handleChange}
      placeholder="Search..."
    />
  );
}

3. Storing Mutable Values in Custom Hooks#

hljs jsx
import { useRef, useCallback } from 'react';

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  });
  
  return ref.current;
}

function useIsFirstRender() {
  const isFirst = useRef(true);
  
  if (isFirst.current) {
    isFirst.current = false;
    return true;
  }
  
  return false;
}

// Usage
function MyComponent() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  const isFirst = useIsFirstRender();
  
  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <p>Is first render: {isFirst ? 'Yes' : 'No'}</p>
    </div>
  );
}

4. Managing Focus in Forms#

hljs jsx
import React, { useRef, useState } from 'react';

function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(0);
  const inputRefs = useRef([]);
  
  const steps = [
    { name: 'Personal Info', fields: ['firstName', 'lastName'] },
    { name: 'Contact Info', fields: ['email', 'phone'] },
    { name: 'Address', fields: ['street', 'city', 'zip'] }
  ];

  const nextStep = () => {
    if (currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1);
      // Focus first input of next step
      setTimeout(() => {
        const firstInput = inputRefs.current[steps[currentStep + 1].fields[0]];
        firstInput?.focus();
      }, 0);
    }
  };

  const prevStep = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1);
    }
  };

  return (
    <div>
      <h2>{steps[currentStep].name}</h2>
      {steps[currentStep].fields.map(field => (
        <input
          key={field}
          ref={el => inputRefs.current[field] = el}
          placeholder={field}
        />
      ))}
      <button onClick={prevStep} disabled={currentStep === 0}>
        Previous
      </button>
      <button onClick={nextStep} disabled={currentStep === steps.length - 1}>
        Next
      </button>
    </div>
  );
}

5. Implementing Click Outside Detection#

hljs jsx
import React, { useRef, useEffect, useState } from 'react';

function useClickOutside(callback) {
  const ref = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, [callback]);

  return ref;
}

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useClickOutside(() => setIsOpen(false));

  return (
    <div ref={dropdownRef} style={{ position: 'relative' }}>
      <button onClick={() => setIsOpen(!isOpen)}>
        Toggle Dropdown
      </button>
      {isOpen && (
        <div style={{ position: 'absolute', top: '100%', border: '1px solid #ccc' }}>
          <div>Option 1</div>
          <div>Option 2</div>
          <div>Option 3</div>
        </div>
      )}
    </div>
  );
}

Performance Optimization with useRef#

1. Avoiding Expensive Re-calculations#

hljs jsx
import React, { useState, useRef, useMemo } from 'react';

function ExpensiveComponent({ data }) {
  const [filter, setFilter] = useState('');
  const expensiveCalculationRef = useRef(null);
  
  // Only recalculate when data changes, not when filter changes
  const expensiveResult = useMemo(() => {
    console.log('Expensive calculation running...');
    return data.map(item => ({
      ...item,
      processed: item.value * 1000 // Expensive operation
    }));
  }, [data]);

  // Store the result in ref to avoid re-calculations
  if (!expensiveCalculationRef.current) {
    expensiveCalculationRef.current = expensiveResult;
  }

  const filteredData = expensiveResult.filter(item => 
    item.name.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter..."
      />
      {filteredData.map(item => (
        <div key={item.id}>{item.name}: {item.processed}</div>
      ))}
    </div>
  );
}

2. Storing Callback References#

hljs jsx
import React, { useRef, useCallback, useState } from 'react';

function OptimizedList({ items }) {
  const [selectedId, setSelectedId] = useState(null);
  const callbacksRef = useRef(new Map());

  const getCallback = useCallback((id) => {
    if (!callbacksRef.current.has(id)) {
      callbacksRef.current.set(id, () => setSelectedId(id));
    }
    return callbacksRef.current.get(id);
  }, []);

  return (
    <div>
      {items.map(item => (
        <Item
          key={item.id}
          item={item}
          onClick={getCallback(item.id)}
          isSelected={selectedId === item.id}
        />
      ))}
    </div>
  );
}

Common Mistakes with useRef#

Expecting useRef changes to trigger re-renders#

hljs jsx
// WRONG: This won't update the UI
function BadComponent() {
  const countRef = useRef(0);
  
  const increment = () => {
    countRef.current += 1; // This won't cause a re-render
  };
  
  return (
    <div>
      <p>Count: {countRef.current}</p> {/* This will always show 0 */}
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// CORRECT: Use useState for reactive updates
function GoodComponent() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1); // This will cause a re-render
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

Using useRef for dynamic UI updates#

hljs jsx
// WRONG: Don't use useRef for UI state
function BadComponent() {
  const isVisibleRef = useRef(false);
  
  const toggle = () => {
    isVisibleRef.current = !isVisibleRef.current; // Won't update UI
  };
  
  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      {isVisibleRef.current && <div>I'm visible!</div>} {/* Won't work */}
    </div>
  );
}

// CORRECT: Use useState for UI state
function GoodComponent() {
  const [isVisible, setIsVisible] = useState(false);
  
  const toggle = () => {
    setIsVisible(!isVisible); // This will update the UI
  };
  
  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      {isVisible && <div>I'm visible!</div>}
    </div>
  );
}

Not initializing useRef.current properly#

hljs jsx
// WRONG: Can cause undefined errors
function BadComponent() {
  const inputRef = useRef(); // No initial value
  
  useEffect(() => {
    inputRef.current.focus(); // Error: Cannot read property 'focus' of undefined
  }, []);
  
  return <input ref={inputRef} />;
}

// CORRECT: Initialize with null
function GoodComponent() {
  const inputRef = useRef(null); // Initialize with null
  
  useEffect(() => {
    inputRef.current?.focus(); // Safe with optional chaining
  }, []);
  
  return <input ref={inputRef} />;
}

Testing useRef#

Testing Components with useRef#

hljs jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';

test('input should be focused on mount', () => {
  render(<LoginForm />);
  
  const input = screen.getByPlaceholderText('Enter your username');
  expect(input).toHaveFocus();
});

test('should focus input when button is clicked', () => {
  render(<LoginForm />);
  
  const input = screen.getByPlaceholderText('Enter your username');
  const button = screen.getByText('Focus Input');
  
  fireEvent.click(button);
  expect(input).toHaveFocus();
});

Testing Custom Hooks with useRef#

hljs jsx
import { renderHook, act } from '@testing-library/react-hooks';
import { usePrevious } from './usePrevious';

test('usePrevious should return previous value', () => {
  const { result, rerender } = renderHook(
    ({ value }) => usePrevious(value),
    { initialProps: { value: 0 } }
  );
  
  expect(result.current).toBeUndefined();
  
  rerender({ value: 1 });
  expect(result.current).toBe(0);
  
  rerender({ value: 2 });
  expect(result.current).toBe(1);
});

Best Practices#

  1. Always initialize useRef with a default value to avoid undefined errors
  2. Use optional chaining when accessing ref.current to prevent runtime errors
  3. Don't use useRef for values that should trigger re-renders - use useState instead
  4. Clean up timers and intervals in useEffect cleanup functions
  5. Use useRef for DOM manipulation and storing mutable values that don't affect UI
  6. Consider using useRef for performance optimization when you need to avoid expensive re-calculations

Conclusion: When Should You Use useRef?#

  • Use useRef when you need persistent values without triggering re-renders
  • Use useState when changes should trigger UI updates
  • useRef is excellent for handling DOM elements, storing previous values, and managing timers

By mastering useRef, you unlock a powerful tool that optimizes performance and keeps your components clean and efficient. Next time you need to store a value without causing a re-render, reach for useRef.

Key Takeaways#

  1. useRef persists values across renders without causing re-renders
  2. DOM manipulation is a primary use case for useRef
  3. Performance optimization through avoiding unnecessary re-calculations
  4. Timer management without affecting component state
  5. Previous value tracking for comparison purposes

Next Steps#

  • Practice implementing the examples in this guide
  • Experiment with custom hooks that use useRef
  • Learn about other React hooks like useImperativeHandle
  • Explore advanced patterns like ref forwarding

Master useRef and you'll have another powerful tool in your React development arsenal!