Refs provide an escape hatch from React's declarative model, allowing direct access to DOM elements and mutable values that don't trigger re-renders. While most React development relies on props and state for data flow, refs serve specialized purposes where imperative code is necessary.
This guide covers the complete ref ecosystem: creating refs with useRef, forwarding refs through components with forwardRef, customizing exposed APIs with useImperativeHandle, and TypeScript best practices for type-safe ref usage.
For applications requiring tight integration with browser APIs or third-party libraries that demand imperative access, understanding refs is essential for building professional React applications.
Understanding the Escape Hatch
React's declarative model asks you to describe what the UI should look like based on current state, and React handles the updates. But some scenarios require imperative access--directly reading DOM properties, managing focus, or integrating with non-React libraries.
Refs are that escape hatch. They give you a handle to persist values across renders without causing updates, and access to underlying DOM nodes when needed.
When Refs Are Appropriate
Refs solve problems that state cannot:
- Direct DOM access: Focus management, scroll position, measuring element dimensions, text selection
- Third-party integration: Canvas libraries, animation libraries, DOM-based frameworks
- Mutable values that don't render: Timers, counters, previous values for comparisons
- Imperative animations: Triggering animations programmatically
As explained in the React documentation on refs, refs are an escape hatch for cases where you need to work with values that exist outside React's rendering model.
The Ref Object
A ref is a container with a mutable .current property. Unlike state, changing .current does not trigger a re-render. The ref persists across renders, holding its value between updates.
Refs vs State
| Aspect | State | Refs |
|---|---|---|
| Triggers render | Yes | No |
| Update method | Setter function | Direct assignment |
| Async updates | Yes (batched) | Immediate |
| Persists across renders | Yes | Yes |
| Primary use | UI that reflects data | Escape hatch scenarios |
When you're unsure whether something should be state or a ref, ask: "Does changing this need to update the UI?" If yes, use state. If you're just holding a value for later use without UI changes, a ref works.
Understanding this distinction is fundamental to writing performant React components. Our React hooks guide covers how state and refs work together in modern React applications.
The useRef Hook
Basic Usage
The useRef hook creates a ref with an initial value:
import { useRef } from 'react';
const myRef = useRef(initialValue);
The returned object has a .current property containing the current value. You can read and write to it directly.
TypeScript Typing
In TypeScript, useRef accepts a type parameter for the initial value:
const countRef = useRef<number>(0);
const inputRef = useRef<HTMLInputElement>(null);
const configRef = useRef<{ theme: string; fontSize: number }>({
theme: 'dark',
fontSize: 14
});
As highlighted in TypeScript-focused React guidance, always type your refs explicitly. For DOM refs, use the appropriate element type to access element-specific properties and methods.
// Good: Specific element type
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Avoid: Generic type loses element-specific properties
const badRef = useRef<HTMLElement>(null);
Ref Callbacks
For complex scenarios, you can pass a function that receives the DOM node when it's mounted:
<input
ref={(node) => {
if (node) {
node.focus();
}
}}
/>
This callback pattern is useful when you need to perform setup or teardown logic based on element lifecycle.
DOM Manipulation with Refs
Common DOM Operations
Refs provide access to native DOM methods and properties:
- Focus management:
inputRef.current.focus(),inputRef.current.blur() - Scroll:
containerRef.current.scrollTop = 0,element.scrollIntoView() - Measurements:
element.getBoundingClientRect(),element.offsetHeight - Selection:
inputRef.current.setSelectionRange(0, 10)
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="email" />;
}
Avoiding Direct DOM Manipulation
While refs give you DOM access, resist the temptation to use them as a replacement for React's reconciliation. Direct DOM manipulation bypasses React's rendering and can lead to inconsistent state. Use refs for operations that genuinely require imperative DOM access, not for routine styling or content updates.
// Anti-pattern: Direct DOM manipulation
function BadComponent() {
const divRef = useRef<HTMLDivElement>(null);
const updateStyle = () => {
divRef.current!.style.backgroundColor = 'red';
};
return <div ref={divRef}>Content</div>;
}
// Better: Use state and CSS classes
function GoodComponent() {
const [isActive, setIsActive] = useState(false);
return (
<div className={isActive ? 'active' : ''}>
Content
</div>
);
}
This principle applies to all DOM operations. Our React performance optimization guide covers when to use imperative patterns versus React's declarative approach.
forwardRef: Component Ref Forwarding
The Problem
Function components receive props but not refs by default. When you wrap an input in a custom component, the parent can't directly access the DOM input.
// CustomInput can't receive ref - it's not in the props interface
function CustomInput({ placeholder, value, onChange }) {
return <input placeholder={placeholder} value={value} onChange={onChange} />;
}
// Parent can't focus this input
function Parent() {
const inputRef = useRef(null);
return <CustomInput ref={inputRef} />;
}
The Solution: forwardRef
forwardRef wraps your component to receive the ref as the second argument:
import { forwardRef } from 'react';
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
function CustomInput({ placeholder, value, onChange }, ref) {
return (
<input
ref={ref}
placeholder={placeholder}
value={value}
onChange={onChange}
/>
);
}
);
Now the parent can access the DOM input with focus(), select(), and other methods. This pattern is essential for building reusable component libraries that expose DOM APIs to consumers.
TypeScript with forwardRef
Proper typing requires specifying both props and ref types: forwardRef<RefType, PropsType>:
import { forwardRef } from 'react';
interface ButtonProps {
variant: 'primary' | 'secondary';
onClick?: () => void;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
function Button({ variant, onClick }, ref) {
return (
<button
ref={ref}
className={`btn btn-${variant}`}
onClick={onClick}
>
Click me
</button>
);
}
);
When to Use forwardRef
Forward refs are essential for:
- Form components: Input, textarea, select wrappers that need external focus control
- Layout components: Container refs for measurements or scroll management
- Component libraries: Exposing DOM APIs to consumers
- Higher-order components: Preserving ref behavior through wrapper layers
Building custom form components is a core part of our custom web application development services.
useImperativeHandle: Customizing Refs
Purpose
Sometimes you don't want to expose the full DOM element through a ref. useImperativeHandle lets you define a custom interface:
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { inputRef.current!.value = ''; },
getValue: () => inputRef.current?.value
}), []);
As documented in the React useImperativeHandle reference, this hook is invaluable for creating clean, intentional APIs for your components.
Syntax
useImperativeHandle(ref, createHandle, dependencies?)
ref: The ref passed from forwardRefcreateHandle: Function returning the custom handle objectdependencies: Array of values that trigger handle recreation when changed
TypeScript Patterns
The custom handle should be properly typed:
interface CustomInputHandle {
focus: () => void;
clear: () => void;
select: () => void;
value: string | undefined;
}
const CustomInput = forwardRef<CustomInputHandle, Props>(
function CustomInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { inputRef.current!.value = ''; },
select: () => { inputRef.current?.select(); },
get value() { return inputRef.current?.value; }
}), []);
return <input ref={inputRef} {...props} />;
}
);
Common Use Cases
- Form validation: Expose validate() and getErrors() methods
- Media controls: Expose play(), pause(), seek() for video/audio
- Complex components: Hide internal complexity behind simple API
- Security boundaries: Prevent access to dangerous DOM methods
Performance Optimization
Refs for Performance
Refs can prevent expensive re-renders when used strategically:
function ExpensiveComponent({ data }) {
const prevDataRef = useRef();
if (data !== prevDataRef.current) {
const result = processData(data);
prevDataRef.current = data;
}
return <div>{/* Render result */}</div>;
}
By storing the previous value in a ref, you can perform comparisons without triggering additional renders.
Avoiding Unnecessary Effects
For timers and subscriptions, refs help clean up properly:
const intervalRef = useRef<number | null>(null);
useEffect(() => {
intervalRef.current = window.setInterval(() => {
setTime(t => t + 1);
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
The ref stores the interval ID for cleanup in the effect's return function, preventing memory leaks.
Memoization with Refs
Refs work with useMemo and useCallback for optimization:
function SearchableList({ items }) {
const listRef = useRef<HTMLUListElement>(null);
const [query, setQuery] = useState('');
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]);
const scrollToTop = () => {
listRef.current?.scrollTo(0, 0);
};
return (
<ul ref={listRef}>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Performance patterns like these are covered in depth in our React hooks guide, which explores advanced optimization techniques.
Best Practices
DO
- Use refs for DOM access when React doesn't provide a declarative API
- Type DOM refs with specific element types
- Check for null before accessing ref.current
- Clean up refs in effects (clear intervals, remove listeners)
- Use forwardRef when creating reusable components
- Define custom handles with useImperativeHandle for complex APIs
DON'T
- Use refs as a replacement for state that should trigger renders
- Read or write ref.current during render (causes stale values)
- Manipulate DOM directly when React provides a way
- Expose full DOM refs without considering security
- Forget that refs are mutable and can change without warning
Following these best practices ensures your components remain maintainable and performant. As noted in TypeScript ref best practices, proper typing and null checking are essential for production-ready code.
Common Patterns
Measuring Element Dimensions
function MeasurableComponent() {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const measure = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
};
measure();
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, []);
return <div ref={containerRef}>Width: {dimensions.width}px</div>;
}
Managing Focus in Forms
function RegistrationForm() {
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const confirmRef = useRef<HTMLInputElement>(null);
const validateAndFocus = () => {
if (!emailRef.current?.value) {
emailRef.current.focus();
return false;
}
if (!passwordRef.current?.value) {
passwordRef.current.focus();
return false;
}
if (passwordRef.current.value !== confirmRef.current?.value) {
confirmRef.current.focus();
return false;
}
return true;
};
return (
<form onSubmit={(e) => {
e.preventDefault();
validateAndFocus();
}}>
<input ref={emailRef} type="email" placeholder="Email" />
<input ref={passwordRef} type="password" placeholder="Password" />
<input ref={confirmRef} type="password" placeholder="Confirm" />
<button type="submit">Submit</button>
</form>
);
}
Combining forwardRef with useImperativeHandle
interface SearchInputHandle {
focus: () => void;
clear: () => void;
isEmpty: () => boolean;
}
const SearchInput = forwardRef<SearchInputHandle, SearchInputProps>(
function SearchInput({ onSearch, ...props }, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
isEmpty: () => !inputRef.current?.value
}), []);
return <input ref={inputRef} {...props} />;
}
);
This pattern creates a clean, intentional API for form inputs. For complex form implementations, consider our custom web application development services.
Summary
Refs are a powerful but specialized tool in React. Use them when you need:
- Direct DOM access (focus, measurements, selections)
- Imperative APIs for complex components (with useImperativeHandle)
- Mutable values that persist without triggering renders
- Integration with non-React libraries
For most UI state, prefer state and props. Reserve refs for the escape hatch scenarios where React's declarative model doesn't fit the problem.
Key Takeaways
- Refs bypass the render cycle -- use sparingly and only when necessary
- Use forwardRef to pass refs through reusable components
- useImperativeHandle creates custom ref APIs for cleaner component interfaces
- Always type refs properly in TypeScript for full type safety
- Clean up refs in effects to prevent memory leaks and resource dangling
Mastering refs is essential for building professional React applications. Combine this knowledge with our React hooks and React performance guides for a complete understanding of modern React development.
Ready to implement these patterns in your project? Our web development services can help you build robust, performant React applications.
Sources
- React Documentation: Referencing Values with Refs - Official documentation covering useRef basics, escape hatch concept, ref vs state differences
- React Documentation: useImperativeHandle - Official reference for customizing exposed ref handles with syntax and examples
- LogRocket: React forwardRef explained - Comprehensive guide covering forwardRef patterns, React 19 updates, practical examples
- Angular Minds: React refs with TypeScript - TypeScript-specific best practices for typing refs