Refs in React (useRef, createRef, forwardRef)
What are Refs?
Refs (references) are a way to get direct access to DOM elements or React components from code.
Refs are used when you need to:
- Manage focus, text selection
- Trigger animations
- Integrate with third-party libraries
- Measure element dimensions
useRef
useRef is a hook for creating refs in functional components.
Accessing DOM elements
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
function handleClick() {
// Get direct access to input
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
Storing mutable value
useRef can be used to store any value that doesn't trigger re-render:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
function stop() {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
useEffect(() => {
return () => stop(); // Cleanup
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
useRef vs useState
// useState - triggers re-render
const [value, setValue] = useState(0);
// useRef - does NOT trigger re-render
const valueRef = useRef(0);
valueRef.current = 1; // Won't trigger render
createRef
createRef is used in class components:
class TextInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
handleClick = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.handleClick}>Focus</button>
</div>
);
}
}
Important:
Don't use createRef in functional components! A new ref will be created on every render. Use useRef.
forwardRef
forwardRef allows passing a ref through a component to its child.
Problem
function CustomInput(props) {
return <input {...props} />;
}
// Doesn't work! ref is not forwarded
function Parent() {
const inputRef = useRef(null);
return <CustomInput ref={inputRef} />; // Error!
}
Solution
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</div>
);
}
useImperativeHandle
useImperativeHandle lets you customize what's accessible to the parent through ref.
Without useImperativeHandle
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} />;
});
// Parent gets access to the entire DOM input
With useImperativeHandle
import { forwardRef, useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose these methods
focus: () => {
inputRef.current.focus();
},
scrollIntoView: () => {
inputRef.current.scrollIntoView();
}
// value, blur and other methods are not accessible
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // Works
// inputRef.current.value; // undefined!
}
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</div>
);
}
Practical examples
Autofocus on mount
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
Measuring element dimensions
function MeasureComponent() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({});
useEffect(() => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={divRef} style={{ padding: 20, backgroundColor: 'lightblue' }}>
Measure me
</div>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
);
}
Third-party library integration
function VideoPlayer({ src }) {
const videoRef = useRef(null);
useEffect(() => {
// Initialize player with third-party library
const player = new ThirdPartyPlayer(videoRef.current);
player.load(src);
return () => {
player.destroy();
};
}, [src]);
return <video ref={videoRef} />;
}
Previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Callback Refs
Alternative way to work with refs using callback function:
function Component() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<div ref={measuredRef}>
<p>This div's height is {height}px</p>
</div>
</div>
);
}
When NOT to use Refs
Don't use for what can be done declaratively
// Bad
function Dialog() {
const dialogRef = useRef(null);
function open() {
dialogRef.current.style.display = 'block';
}
function close() {
dialogRef.current.style.display = 'none';
}
return <div ref={dialogRef}>Dialog</div>;
}
// Good
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
return isOpen ? <div>Dialog</div> : null;
}
Don't store data that affects rendering
// Bad
function Component() {
const dataRef = useRef([]);
function addItem(item) {
dataRef.current.push(item); // Component won't re-render!
}
return <div>{dataRef.current.length} items</div>;
}
// Good
function Component() {
const [data, setData] = useState([]);
function addItem(item) {
setData(prev => [...prev, item]); // Re-render will happen
}
return <div>{data.length} items</div>;
}
TypeScript typing
import { useRef, forwardRef, useImperativeHandle } from 'react';
// useRef with DOM element
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return <input ref={inputRef} />;
}
// forwardRef
interface Props {
placeholder?: string;
}
const CustomInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
// useImperativeHandle
interface CustomInputHandle {
focus: () => void;
reset: () => void;
}
const CustomInput = forwardRef<CustomInputHandle, Props>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
reset() {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} {...props} />;
});
Common mistakes
Accessing ref.current before mounting
// Wrong
function Component() {
const ref = useRef(null);
console.log(ref.current); // null! Element not created yet
return <div ref={ref}>Hello</div>;
}
// Correct
function Component() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // Element is accessible
}, []);
return <div ref={ref}>Hello</div>;
}
Creating new ref on every render
// Wrong
function Component() {
const ref = createRef(); // New ref on every render!
return <div ref={ref}>Hello</div>;
}
// Correct
function Component() {
const ref = useRef(null); // Same ref
return <div ref={ref}>Hello</div>;
}
Conclusion
Refs in React:
useRef— for functional componentscreateRef— for class componentsforwardRef— for forwarding ref through componentuseImperativeHandle— for controlling ref access- Don't trigger re-render on change
- Use when declarative approach is impossible
- Avoid for what can be done through state
In interviews:
Important to be able to:
- Explain what refs are and when to use them
- Show difference between useRef and useState
- Explain what forwardRef and useImperativeHandle are for
- Give examples of proper ref usage
- Describe when NOT to use refs