useRef Hook in React
The useRef hook is a powerful React hook that allows you to persist values between renders without causing re-renders and provides a way to access DOM elements directly.
Table of Contents
- useRef Hook in React
What is useRef?
useRef is a React hook that returns a mutable ref object with a .current property that can be used to store values that persist across renders without causing re-renders.
Key Concepts:
- Mutable Reference: The
.currentproperty can be changed without triggering re-renders - Persistent Values: Values stored in refs persist between component renders
- DOM Access: Can be used to directly access DOM elements
- Performance: Useful for storing values that don’t need to trigger re-renders
useRef vs useState:
// useState: Triggers re-render when value changes
function StateExample() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // This triggers a re-render
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// useRef: No re-render when value changes
function RefExample() {
const countRef = useRef(0);
const increment = () => {
countRef.current += 1; // This does NOT trigger a re-render
console.log('Count:', countRef.current);
};
return (
<div>
<p>Count: {countRef.current}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Basic Usage
1. Creating and Using Refs:
import React, { useRef, useEffect } from 'react';
function BasicRefExample() {
// Create a ref with initial value
const countRef = useRef(0);
const inputRef = useRef(null);
const previousValueRef = useRef(null);
// Access and modify ref values
const handleClick = () => {
countRef.current += 1;
console.log('Current count:', countRef.current);
};
// Focus input on mount
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
// Store previous value
const handleInputChange = (e) => {
previousValueRef.current = e.target.value;
};
return (
<div>
<h3>Basic Ref Example</h3>
<div>
<button onClick={handleClick}>
Increment Count (Check console)
</button>
<p>Count: {countRef.current}</p>
</div>
<div>
<input
ref={inputRef}
onChange={handleInputChange}
placeholder="This will be focused on mount"
/>
</div>
<div>
<p>Previous value: {previousValueRef.current}</p>
</div>
</div>
);
}
2. DOM Element Access:
function DOMAccessExample() {
const buttonRef = useRef(null);
const divRef = useRef(null);
const handleButtonClick = () => {
// Access DOM element properties
if (buttonRef.current) {
console.log('Button text:', buttonRef.current.textContent);
console.log('Button position:', buttonRef.current.getBoundingClientRect());
// Modify DOM element
buttonRef.current.style.backgroundColor = 'red';
}
};
const handleDivClick = () => {
if (divRef.current) {
// Scroll to element
divRef.current.scrollIntoView({ behavior: 'smooth' });
// Get computed styles
const styles = window.getComputedStyle(divRef.current);
console.log('Background color:', styles.backgroundColor);
}
};
return (
<div>
<h3>DOM Access Example</h3>
<button ref={buttonRef} onClick={handleButtonClick}>
Click me to see DOM info
</button>
<div
ref={divRef}
onClick={handleDivClick}
style={{
width: '200px',
height: '100px',
backgroundColor: 'lightblue',
margin: '20px 0',
cursor: 'pointer'
}}
>
Click to scroll to this div
</div>
<div style={{ height: '500px', backgroundColor: 'lightgray' }}>
Scroll down to see the div above
</div>
</div>
);
}
3. Storing Previous Values:
function PreviousValueExample() {
const [count, setCount] = useState(0);
const previousCountRef = useRef();
useEffect(() => {
// Store previous value before updating
previousCountRef.current = count;
});
return (
<div>
<h3>Previous Value Example</h3>
<p>Current count: {count}</p>
<p>Previous count: {previousCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
Common Use Cases
1. Focus Management:
function FocusManagementExample() {
const inputRef = useRef(null);
const buttonRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
const focusButton = () => {
buttonRef.current?.focus();
};
const selectInputText = () => {
if (inputRef.current) {
inputRef.current.select();
}
};
return (
<div>
<h3>Focus Management</h3>
<input
ref={inputRef}
type="text"
placeholder="Type something..."
defaultValue="Sample text"
/>
<button ref={buttonRef}>
Button
</button>
<div style={{ marginTop: '10px' }}>
<button onClick={focusInput}>Focus Input</button>
<button onClick={focusButton}>Focus Button</button>
<button onClick={selectInputText}>Select Input Text</button>
</div>
</div>
);
}
2. Timer Management:
function TimerExample() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current) return; // Prevent multiple timers
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const resetTimer = () => {
stopTimer();
setCount(0);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<h3>Timer Example</h3>
<p>Count: {count}</p>
<button onClick={startTimer}>Start Timer</button>
<button onClick={stopTimer}>Stop Timer</button>
<button onClick={resetTimer}>Reset Timer</button>
</div>
);
}
3. Form Validation:
function FormValidationExample() {
const [errors, setErrors] = useState({});
const formRef = useRef(null);
const validateForm = () => {
const formData = new FormData(formRef.current);
const newErrors = {};
// Validate required fields
if (!formData.get('name')) {
newErrors.name = 'Name is required';
}
if (!formData.get('email')) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.get('email'))) {
newErrors.email = 'Invalid email format';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form is valid, submitting...');
// Submit form logic here
} else {
console.log('Form has errors');
}
};
return (
<div>
<h3>Form Validation Example</h3>
<form ref={formRef} onSubmit={handleSubmit}>
<div>
<input
name="name"
placeholder="Name"
style={{ border: errors.name ? '1px solid red' : '1px solid black' }}
/>
{errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
</div>
<div>
<input
name="email"
type="email"
placeholder="Email"
style={{ border: errors.email ? '1px solid red' : '1px solid black' }}
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<button type="submit">Submit</button>
</form>
</div>
);
}
4. Animation Control:
function AnimationExample() {
const elementRef = useRef(null);
const animationRef = useRef(null);
const startAnimation = () => {
if (elementRef.current) {
elementRef.current.style.transition = 'all 0.5s ease';
elementRef.current.style.transform = 'translateX(200px) rotate(360deg)';
}
};
const stopAnimation = () => {
if (elementRef.current) {
elementRef.current.style.transition = 'none';
elementRef.current.style.transform = 'translateX(0) rotate(0deg)';
}
};
const pulseAnimation = () => {
if (elementRef.current) {
elementRef.current.style.animation = 'pulse 1s infinite';
}
};
return (
<div>
<h3>Animation Example</h3>
<div
ref={elementRef}
style={{
width: '100px',
height: '100px',
backgroundColor: 'blue',
margin: '20px 0'
}}
/>
<button onClick={startAnimation}>Start Animation</button>
<button onClick={stopAnimation}>Stop Animation</button>
<button onClick={pulseAnimation}>Pulse Animation</button>
<style>
{`
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
`}
</style>
</div>
);
}
Advanced Patterns
1. Custom Hook for Previous Value:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Usage
function PreviousValueHook() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<h3>Previous Value Hook</h3>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
2. useRef with useImperativeHandle:
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
select: () => inputRef.current?.select(),
getValue: () => inputRef.current?.value,
setValue: (value) => {
if (inputRef.current) {
inputRef.current.value = value;
}
}
}));
return <input ref={inputRef} {...props} />;
});
// Usage
function ImperativeHandleExample() {
const inputRef = useRef(null);
const handleFocus = () => inputRef.current?.focus();
const handleBlur = () => inputRef.current?.blur();
const handleSelect = () => inputRef.current?.select();
const handleGetValue = () => console.log('Value:', inputRef.current?.getValue());
const handleSetValue = () => inputRef.current?.setValue('New value');
return (
<div>
<h3>Imperative Handle Example</h3>
<CustomInput ref={inputRef} placeholder="Custom input" />
<div style={{ marginTop: '10px' }}>
<button onClick={handleFocus}>Focus</button>
<button onClick={handleBlur}>Blur</button>
<button onClick={handleSelect}>Select</button>
<button onClick={handleGetValue}>Get Value</button>
<button onClick={handleSetValue}>Set Value</button>
</div>
</div>
);
}
3. useRef for Callback Functions:
function CallbackRefExample() {
const callbackRef = useRef();
const handleClick = () => {
if (callbackRef.current) {
callbackRef.current();
}
};
const setCallback = (callback) => {
callbackRef.current = callback;
};
return (
<div>
<h3>Callback Ref Example</h3>
<button onClick={handleClick}>Execute Callback</button>
<button onClick={() => setCallback(() => alert('Hello!'))}>
Set Alert Callback
</button>
<button onClick={() => setCallback(() => console.log('Logged!'))}>
Set Log Callback
</button>
</div>
);
}
4. useRef for Instance Variables:
function InstanceVariableExample() {
const renderCountRef = useRef(0);
const lastRenderTimeRef = useRef(Date.now());
// Track render count
renderCountRef.current += 1;
const handleClick = () => {
const now = Date.now();
const timeSinceLastRender = now - lastRenderTimeRef.current;
console.log(`Render #${renderCountRef.current}`);
console.log(`Time since last render: ${timeSinceLastRender}ms`);
lastRenderTimeRef.current = now;
};
return (
<div>
<h3>Instance Variable Example</h3>
<p>Render count: {renderCountRef.current}</p>
<button onClick={handleClick}>Click to see render info</button>
</div>
);
}
Best Practices
1. Use useRef for Values That Don’t Need Re-renders:
// Good: Use ref for values that don't affect UI
function GoodExample() {
const timeoutRef = useRef(null);
const [count, setCount] = useState(0);
const startTimeout = () => {
timeoutRef.current = setTimeout(() => {
console.log('Timeout completed');
}, 1000);
};
const clearTimeout = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={startTimeout}>Start Timeout</button>
<button onClick={clearTimeout}>Clear Timeout</button>
</div>
);
}
// Bad: Using state for values that don't affect UI
function BadExample() {
const [timeoutId, setTimeoutId] = useState(null);
const [count, setCount] = useState(0);
const startTimeout = () => {
const id = setTimeout(() => {
console.log('Timeout completed');
}, 1000);
setTimeoutId(id); // Unnecessary re-render
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={startTimeout}>Start Timeout</button>
</div>
);
}
2. Clean Up Refs in useEffect:
// Good: Clean up refs properly
function GoodCleanupExample() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return <div>Component with cleanup</div>;
}
// Bad: No cleanup
function BadCleanupExample() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
// Missing cleanup - memory leak!
}, []);
return <div>Component without cleanup</div>;
}
3. Check Ref Existence Before Use:
// Good: Check ref existence
function GoodRefCheck() {
const inputRef = useRef(null);
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
// Bad: No existence check
function BadRefCheck() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // Could throw error if ref is null
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
4. Use Refs for DOM Measurements:
// Good: Use ref for DOM measurements
function GoodDOMExample() {
const elementRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (elementRef.current) {
const rect = elementRef.current.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height
});
}
}, []);
return (
<div>
<div
ref={elementRef}
style={{ width: '200px', height: '100px', backgroundColor: 'blue' }}
/>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
);
}
Common Interview Questions
Q: What is useRef and when should you use it?
- useRef is a hook that returns a mutable ref object used to persist values between renders without causing re-renders.
Q: What’s the difference between useRef and useState?
- useRef doesn’t trigger re-renders when the value changes, while useState does.
Q: How do you access DOM elements with useRef?
- Pass the ref to the ref prop of a DOM element, then access it via ref.current.
Q: Can you modify a ref value directly?
- Yes, you can modify ref.current directly without triggering re-renders.
Q: What are common use cases for useRef?
- DOM element access, storing previous values, timer management, and form validation.
Q: How do you clean up refs?
- Use useEffect cleanup functions to clear timers, intervals, or other resources stored in refs.
Q: Can you use useRef with functional components?
- Yes, useRef works with both functional and class components (via React.createRef).
Q: What happens if you don’t check if a ref exists?
- You might get runtime errors if trying to access properties of null/undefined.
Q: How do you create a custom hook with useRef?
- Create a function that starts with “use” and returns a ref or uses refs internally.
Q: When should you avoid using useRef?
- When the value needs to trigger re-renders or when you need to track state changes.
Summary
- useRef provides a way to persist values between renders without causing re-renders
- Common uses include DOM access, timer management, and storing previous values
- Best practices include proper cleanup, existence checks, and using refs for non-UI values
- Advanced patterns include custom hooks, imperative handles, and callback refs
- Performance benefits come from avoiding unnecessary re-renders
- Understanding useRef is essential for advanced React development and performance optimization
Interview angle
- “What is
useRefactually for?” - a mutable box that persists across renders and does not trigger one when it changes. Two uses: a handle on a DOM node, and instance-like values such as timer IDs, previous values, or a mutable flag. - “When does a ref belong instead of state?” - when the value is not rendered. If changing it should update the screen, it is state. Reading a ref during render is also discouraged, because the value is not part of the render’s inputs.
- “What changed in React 19?” - function components receive
refas an ordinary prop, soforwardRefis no longer needed for new code. Ref callbacks can also return a cleanup function, which replaces the null-check dance. - “Why is
ref.currentnull on the first render?” - refs attach after the DOM is created. Read them in an effect or an event handler, not during render.