React.js Fundamentals

useRef Hook - DOM Access and Persistent Values

15 min Lesson 12 of 40

Understanding Refs in React

Refs provide a way to access DOM nodes or React elements created in the render method. They are useful when you need to interact with DOM elements directly or persist values across renders without causing re-renders.

The useRef hook returns a mutable ref object whose .current property is initialized to the passed argument. The returned object persists for the full lifetime of the component.

When to Use Refs

Use refs for: managing focus/text selection/media playback, triggering imperative animations, integrating with third-party DOM libraries, and storing mutable values that don't trigger re-renders.

Basic useRef Syntax

Creating and using a ref is straightforward:

import React, { useRef } from 'react';

function TextInputFocus() {
  // Create a ref to store the input element
  const inputRef = useRef(null);

  const handleFocus = () => {
    // Access the DOM element and focus it
    inputRef.current.focus();
  };

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        placeholder="Click button to focus me"
      />
      <button onClick={handleFocus}>
        Focus Input
      </button>
    </div>
  );
}

export default TextInputFocus;

In this example, inputRef.current holds the actual DOM input element, allowing us to call focus() on it directly.

Accessing DOM Elements

The most common use case for refs is accessing DOM elements to perform operations that React doesn't handle declaratively:

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

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

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

  const handleVolumeChange = (e) => {
    videoRef.current.volume = e.target.value / 100;
  };

  return (
    <div>
      <video
        ref={videoRef}
        src="https://example.com/video.mp4"
        width="400"
      />

      <div>
        <button onClick={togglePlay}>
          {isPlaying ? 'Pause' : 'Play'}
        </button>

        <label>
          Volume:
          <input
            type="range"
            min="0"
            max="100"
            defaultValue="50"
            onChange={handleVolumeChange}
          />
        </label>
      </div>
    </div>
  );
}

export default VideoPlayer;

Persisting Values Without Re-renders

Unlike state, updating a ref's .current property doesn't trigger a re-render. This makes refs perfect for storing values that need to persist but shouldn't affect the UI directly:

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

function RenderCounter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  // Increment render count on every render
  useEffect(() => {
    renderCount.current += 1;
  });

  return (
    <div>
      <h2>Count: {count}</h2>
      <p>This component has rendered {renderCount.current} times</p>

      <button onClick={() => setCount(count + 1)}>
        Increment Count
      </button>
    </div>
  );
}

export default RenderCounter;

useRef vs useState

Use useState when changing the value should trigger a re-render. Use useRef when you need to store a value that persists between renders but shouldn't cause re-renders when it changes.

Storing Previous Values

Refs are excellent for storing the previous value of state or props:

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

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

  useEffect(() => {
    // Store the current count as previous for next render
    prevCountRef.current = count;
  });

  const prevCount = prevCountRef.current;

  return (
    <div>
      <h2>Current Count: {count}</h2>
      <h3>Previous Count: {prevCount ?? 'None'}</h3>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
    </div>
  );
}

export default PreviousValue;

Storing Timeout and Interval IDs

Refs are perfect for storing timer IDs that need to be cleared later:

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

function StopwatchWithRef() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);

  const start = () => {
    if (isRunning) return;

    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setTime(prevTime => prevTime + 1);
    }, 1000);
  };

  const stop = () => {
    if (!isRunning) return;

    setIsRunning(false);
    clearInterval(intervalRef.current);
  };

  const reset = () => {
    stop();
    setTime(0);
  };

  return (
    <div>
      <h2>Time: {time} seconds</h2>

      <button onClick={start} disabled={isRunning}>
        Start
      </button>
      <button onClick={stop} disabled={!isRunning}>
        Stop
      </button>
      <button onClick={reset}>
        Reset
      </button>
    </div>
  );
}

export default StopwatchWithRef;

Common Mistake: Using Refs Instead of State

Don't use refs to store values that affect what appears on screen. If changing a value should update the UI, use state instead. Refs are for values that don't directly affect rendering.

Callback Refs

Instead of passing a ref object, you can pass a function (callback ref) that receives the DOM element as an argument:

import React, { useState, useCallback } from 'react';

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

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

  return (
    <div>
      <h2 ref={measuredRef}>
        This is a heading to measure
      </h2>
      <p>The heading height is: {height}px</p>
    </div>
  );
}

export default MeasureDimensions;

Forwarding Refs

Sometimes you need to pass a ref from a parent component to a child component. Use React.forwardRef for this:

import React, { useRef, forwardRef } from 'react';

// Child component that accepts a forwarded ref
const CustomInput = forwardRef((props, ref) => {
  return (
    <input
      ref={ref}
      type="text"
      className="custom-input"
      {...props}
    />
  );
});

// Parent component that uses the forwarded ref
function ParentComponent() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <CustomInput
        ref={inputRef}
        placeholder="I can be focused from parent"
      />
      <button onClick={focusInput}>
        Focus Custom Input
      </button>
    </div>
  );
}

export default ParentComponent;

useImperativeHandle Hook

Use useImperativeHandle with forwardRef to customize what the parent component can access on the child:

import React, { useRef, useImperativeHandle, forwardRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // Expose only specific methods to parent
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView({ behavior: 'smooth' });
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));

  return <input ref={inputRef} {...props} />;
});

function FormWithFancyInput() {
  const fancyInputRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const value = fancyInputRef.current.getValue();
    console.log('Input value:', value);
    fancyInputRef.current.focus();
  };

  return (
    <form onSubmit={handleSubmit}>
      <FancyInput
        ref={fancyInputRef}
        placeholder="Type something"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export default FormWithFancyInput;

Practical Example: Auto-scrolling Chat

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

function ChatWindow() {
  const [messages, setMessages] = useState([
    { id: 1, text: 'Hello!' },
    { id: 2, text: 'How are you?' }
  ]);
  const [inputValue, setInputValue] = useState('');
  const messagesEndRef = useRef(null);

  // Auto-scroll to bottom when messages change
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const addMessage = (e) => {
    e.preventDefault();
    if (!inputValue.trim()) return;

    setMessages(prev => [
      ...prev,
      { id: Date.now(), text: inputValue }
    ]);
    setInputValue('');
  };

  return (
    <div style={{ height: '300px', display: 'flex', flexDirection: 'column' }}>
      <div style={{ flex: 1, overflow: 'auto', border: '1px solid #ccc', padding: '10px' }}>
        {messages.map(msg => (
          <div key={msg.id} style={{ marginBottom: '10px' }}>
            {msg.text}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={addMessage} style={{ display: 'flex', marginTop: '10px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Type a message..."
          style={{ flex: 1 }}
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

export default ChatWindow;

Exercise 1: Click Counter

Create a component that counts how many times a button has been clicked WITHOUT causing re-renders. Display the count only when a "Show Count" button is clicked.

// Requirements:
// 1. Use useRef to store the click count
// 2. "Click Me" button increments ref without re-render
// 3. "Show Count" button displays count using alert or console
// 4. Component should only re-render when "Show Count" is clicked

Exercise 2: Form Field Focus Manager

Build a multi-field form with "Next" buttons that automatically focus the next input field.

// Requirements:
// 1. Form with name, email, phone, and address fields
// 2. Each field has a "Next" button
// 3. Clicking "Next" focuses the next input
// 4. Last field's button submits the form
// 5. Use refs for all inputs

Exercise 3: Canvas Drawing App

Create a simple drawing component using the HTML5 canvas element and useRef.

// Requirements:
// 1. Canvas element 400x300px
// 2. Draw lines when mouse is pressed and moved
// 3. "Clear Canvas" button to reset
// 4. Use useRef to access canvas and context
// 5. Track mouse state without causing re-renders

Summary

  • useRef returns a mutable object that persists for component lifetime
  • Changing ref.current does NOT trigger re-renders
  • Use refs for DOM access, timers, previous values, and imperative operations
  • forwardRef allows passing refs from parent to child components
  • useImperativeHandle customizes what parent can access via ref
  • Callback refs receive the DOM element as an argument
  • Don't use refs for data that affects rendering - use state instead