useRef Hook - DOM Access and Persistent Values
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
useRefreturns a mutable object that persists for component lifetime- Changing
ref.currentdoes NOT trigger re-renders - Use refs for DOM access, timers, previous values, and imperative operations
forwardRefallows passing refs from parent to child componentsuseImperativeHandlecustomizes 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