Working With Refs In React

A comprehensive guide to mastering refs, from useRef to React 19's simplified ref handling. Build better React components with proper ref patterns.

What Are Refs in React?

Refs provide a way to access DOM nodes or React elements created in the render method. In React's typical declarative dataflow, props are the only way parent components interact with their children. However, there are cases where you need to imperatively modify a child outside of this typical flow.

Refs serve as an escape hatch from React's declarative model, allowing direct DOM access when necessary. While React's unidirectional data flow is powerful for most scenarios, certain use cases require the imperative approach that refs provide.

The Ref Object Lifecycle

Refs follow a predictable lifecycle in React components:

  • Creation: Refs are created using useRef() hook or React.createRef() method
  • Attachment: The ref is attached to a DOM element or component via the ref attribute
  • Population: On mount, React populates the ref's .current property with the DOM node
  • Cleanup: On unmount, the ref is cleaned up (set to null in older versions, or cleanup function in React 19)

Understanding this lifecycle is crucial for proper ref management and avoiding common pitfalls.

useRef Hook

The useRef hook is the primary way to create refs in modern React functional components. It returns a ref object with a mutable .current property that persists across renders without triggering re-renders.

import { useRef } from 'react';

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

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

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

Key Characteristics of useRef

  • Persistent object: Returns the same ref object on every render
  • Mutable current: Changes to .current don't trigger re-renders
  • Initial value: Optionally accepts an initial value that only used on first render
  • Versatile use: Works for both DOM references and storing mutable values

The useRef hook is particularly powerful because it solves two related problems: providing access to DOM elements and storing values that persist between renders without causing re-renders. Understanding how refs work alongside other React hooks is essential for building effective React applications.

Accessing DOM Elements with useRef
1function InputExample() {2 const inputRef = useRef(null);3 4 const handleClick = () => {5 // Access the DOM element directly6 console.log(inputRef.current.value); // Get value7 inputRef.current.focus(); // Focus the input8 inputRef.current.select(); // Select text9 };10 11 return (12 <div>13 <input ref={inputRef} defaultValue="Hello World" />14 <button onClick={handleClick}>Interact with Input</button>15 </div>16 );17}

Callback Refs

Callback refs provide an alternative approach using a callback function instead of a ref object. This gives you more control over when refs are attached and can be useful for dynamic ref scenarios.

function CallbackRefExample() {
 const [element, setElement] = useState(null);

 const refCallback = (node) => {
 // Called when element mounts (node is the DOM element)
 setElement(node);
 // Called when element unmounts (node is null)
 };

 useEffect(() => {
 if (element) {
 element.focus();
 }
 }, [element]);

 return <input ref={refCallback} />;
}

Benefits of Callback Refs

  • Dynamic attachment: Useful when you need to manage multiple refs
  • Immediate access: Know exactly when the ref is attached or detached
  • Integration: Often better for third-party library integration
  • Custom behavior: Implement custom ref logic without creating custom hooks

Callback refs are particularly valuable when integrating React with imperative JavaScript patterns that require precise control over DOM manipulation timing.

forwardRef for Component Refs

The forwardRef API enables components to forward refs they receive to their underlying DOM elements. This is essential for composing components that need to be accessed by their parent components.

import { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => {
 return <input ref={ref} {...props} />;
});

// Usage
const inputRef = useRef(null);
<MyInput ref={inputRef} placeholder="Enter text" />;

Why forwardRef Exists

React prevents passing refs as normal props, so forwardRef creates a special component that receives the ref as a second argument. This enables:

  • Component composition without exposing implementation details
  • Parent components accessing child DOM elements
  • Consistent APIs across component libraries
  • Proper TypeScript type inference for refs

forwardRef with TypeScript

import { forwardRef } from 'react';

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
 label: string;
}

const MyInput = forwardRef<HTMLInputElement, InputProps>(
 ({ label, ...props }, ref) => {
 return (
 <div className="input-wrapper">
 <label>{label}</label>
 <input ref={ref} {...props} />
 </div>
 );
 }
);

useImperativeHandle for Custom Ref APIs

The useImperativeHandle hook customizes what the parent receives when accessing a ref. Instead of exposing the raw DOM element, you can expose a controlled API with specific methods.

import { useImperativeHandle, forwardRef } from 'react';

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

 useImperativeHandle(ref, () => ({
 focus: () => inputRef.current.focus(),
 select: () => inputRef.current.select(),
 getValue: () => inputRef.current.value,
 setValue: (value) => inputRef.current.value = value,
 isEmpty: () => !inputRef.current.value.trim(),
 }));

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

When to useImperativeHandle

  • Exposing specific methods to parent components
  • Creating consistent APIs across similar components
  • Hiding internal implementation details
  • Building component libraries with controlled interfaces

React 19: Ref as Prop

React 19 introduces a significant simplification: refs can now be passed as regular props without needing forwardRef. This reduces boilerplate and makes component composition more natural.

Before React 19 (using forwardRef)

const SelectBox = forwardRef(({ options, onChange }, ref) => (
 <select ref={ref} onChange={onChange}>
 {options.map(option => (
 <option key={option} value={option}>{option}</option>
 ))}
 </select>
));

After React 19 (passing ref as prop)

const SelectBox = ({ options, onChange, ref }) => (
 <select ref={ref} onChange={onChange}>
 {options.map(option => (
 <option key={option} value={option}>{option}</option>
 ))}
 </select>
);

Benefits

  • Less boilerplate: No unnecessary forwardRef wrapping
  • More readable: Code is easier to follow and understand
  • Easier maintenance: Ref interactions are much cleaner
  • Better composition: Ref handling feels more natural

Ref Cleanup Function Example

function App() {
 const setTypeRef = (ref) => {
 if (ref) {
 console.log("Setup ref");
 // Setup code here - add event listeners, etc.
 }
 // Return cleanup function
 return () => {
 console.log("Cleanup function called");
 // Cleanup code here - remove event listeners, etc.
 };
 };

 return <SelectBox ref={setTypeRef} />;
}

TypeScript Changes

With React 19's cleanup functions, TypeScript now requires explicit returns from ref callbacks. Implicit returns are no longer allowed to ensure proper cleanup handling:

// Before - implicit return (no longer works)
const myRef = (node) => node && node.focus();

// After - explicit return required
const myRef = (node) => {
 if (node) {
 node.focus();
 }
 // No implicit return
};
Best Practices for React Refs

Minimize Ref Usage

Use refs only when necessary. Prefer declarative patterns with state and props. Refs are an escape hatch, not a replacement for React's data flow.

Check for null Before Use

Always verify that ref.current exists before accessing it. Unmounted components, conditional rendering, and initial render states can all result in null refs.

Cleanup Properly

Remove event listeners, clear intervals, and destroy instances in cleanup functions. React 19's cleanup functions make this easier than ever.

Type Your Refs

Use TypeScript to type your refs properly. This catches errors at compile time and provides better IDE support and documentation.

Avoid Overusing forwardRef

Only forward refs when parents need DOM access. Consider useImperativeHandle for controlled APIs that hide implementation details.

Consider Performance

Refs can improve performance by avoiding re-renders for frequently changing values. But direct DOM manipulation can also break React's model if overused.

Common Use Cases

Form Focus Management

function LoginForm() {
 const emailRef = useRef(null);
 const passwordRef = useRef(null);

 const handleSubmit = (e) => {
 e.preventDefault();
 emailRef.current.focus();
 };

 const handleInvalid = (e) => {
 e.target.focus();
 };

 return (
 <form onSubmit={handleSubmit}>
 <input
 ref={emailRef}
 type="email"
 onInvalid={handleInvalid}
 required
 />
 <input
 ref={passwordRef}
 type="password"
 onInvalid={handleInvalid}
 required
 />
 <button type="submit">Login</button>
 </form>
 );
}

Integration with Third-Party Libraries

import { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';

function ChartComponent({ data }) {
 const canvasRef = useRef(null);
 const chartRef = useRef(null);

 useEffect(() => {
 if (!canvasRef.current) return;

 chartRef.current = new Chart(canvasRef.current, {
 type: 'bar',
 data,
 });

 return () => {
 if (chartRef.current) {
 chartRef.current.destroy();
 }
 };
 }, []);

 useEffect(() => {
 if (chartRef.current) {
 chartRef.current.data = data;
 chartRef.current.update();
 }
 }, [data]);

 return <canvas ref={canvasRef} />;
}
Media Controls with Refs
1function VideoPlayer({ src }) {2 const videoRef = useRef(null);3 const [isPlaying, setIsPlaying] = useState(false);4 5 const togglePlay = () => {6 if (!videoRef.current) return;7 8 if (videoRef.current.paused) {9 videoRef.current.play();10 setIsPlaying(true);11 } else {12 videoRef.current.pause();13 setIsPlaying(false);14 }15 };16 17 return (18 <div className="video-player">19 <video20 ref={videoRef}21 src={src}22 onPlay={() => setIsPlaying(true)}23 onPause={() => setIsPlaying(false)}24 />25 <div className="controls">26 <button onClick={togglePlay}>27 {isPlaying ? 'Pause' : 'Play'}28 </button>29 <button onClick={() => videoRef.current?.restart()}>30 Restart31 </button>32 </div>33 </div>34 );35}

Storing Mutable Values with useRef

One powerful use of useRef is storing values that change frequently but don't need to trigger re-renders. This is useful for tracking previous values, timer IDs, or any mutable data that shouldn't affect rendering.

Tracking Previous Values

function usePrevious(value) {
 const ref = useRef();

 useEffect(() => {
 ref.current = value;
 }, [value]);

 return ref.current;
}

// Usage
function Counter() {
 const [count, setCount] = useState(0);
 const previousCount = usePrevious(count);

 return (
 <div>
 <p>Current: {count}, Previous: {previousCount}</p>
 <button onClick={() => setCount(c => c + 1)}>Increment</button>
 </div>
 );
}

Timer Management

function Timer() {
 const intervalRef = useRef(null);
 const [count, setCount] = useState(0);

 const startTimer = () => {
 intervalRef.current = setInterval(() => {
 setCount(c => c + 1);
 }, 1000);
 };

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

 useEffect(() => {
 startTimer();
 return stopTimer;
 }, []);

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

Frequently Asked Questions

Conclusion

Refs are an essential tool in React for situations where declarative patterns aren't sufficient. They provide direct access to DOM elements and a way to store mutable values without triggering re-renders.

Key takeaways from this guide:

  1. Refs are an escape hatch: Use them only when necessary, preferring declarative patterns with state and props for most interactions.

  2. useRef is your primary tool: The useRef hook creates persistent ref objects that work for both DOM references and storing mutable values.

  3. forwardRef enables composition: Before React 19, forwardRef was needed for components to accept refs. React 19 simplifies this by allowing refs as regular props.

  4. Clean up properly: Always cleanup refs to prevent memory leaks. React 19's cleanup functions make this easier than ever.

  5. Stay current with React: React 19 introduces significant ref improvements that reduce boilerplate and improve developer experience.

By understanding when and how to use refs properly, you can build more performant React applications that leverage the best of both declarative and imperative patterns. If you're building complex React applications and want to ensure your team follows best practices for component architecture, our web development services can help you establish proper patterns across your codebase. We also specialize in TypeScript integration to help you catch errors at compile time and maintain type safety throughout your React projects.

Need Help Building React Applications?

Our team specializes in building modern React applications with best practices for performance and maintainability. From component architecture to state management, we can help you ship faster.