React components normally update their user interface when their props or state change. But sometimes a component needs to remember a value without causing a re-render, or it needs direct access to a DOM element.
This is where Refs become useful.
A ref is a value that React keeps between renders. Unlike state, changing a ref does not cause the component to render again.
Refs are commonly used for:
- Accessing DOM elements
- Focusing an input
- Controlling media elements
- Storing timer IDs
- Remembering previous values
- Keeping mutable values between renders
- Integrating with browser APIs
- Working with third-party libraries
- Exposing a limited imperative API from a component
The most common Hook for creating a ref is useRef.
What Is useRef?
useRef is a React Hook that creates a mutable object whose value persists across renders.
Import it from React:
import { useRef } from "react";
The basic syntax is:
const ref = useRef(initialValue);
For example:
const countRef = useRef(0);
The returned object looks conceptually like:
{
current: 0
}
You access the stored value through:
countRef.current
You can change it:
countRef.current = 10;
The important difference is that changing:
countRef.current
does not trigger a re-render.
useRef vs useState
This is one of the most important differences to understand.
With state:
const [count, setCount] = useState(0);
When you call:
setCount(10);
React schedules a re-render.
With a ref:
const countRef = useRef(0);
you can write:
countRef.current = 10;
but React does not re-render because of that change.
A simple comparison:
| Feature | useState | useRef |
|---|---|---|
| Persists between renders | Yes | Yes |
| Changing value causes re-render | Yes | No |
| Access value | State variable | .current |
| Designed for UI state | Yes | No |
| Can store mutable values | Yes, through updates | Yes |
| Can reference DOM elements | No | Yes |
| Common for timers | Sometimes | Often |
A useful rule is:
If changing the value should update the UI, use state. If the value needs to persist but does not need to trigger a render, a ref may be appropriate.
Creating a Ref
Here is a simple example:
import { useRef } from "react";
function Example() {
const valueRef = useRef(0);
return <p>Ref created</p>;
}
Initially:
valueRef.current
contains:
0
You can update it from an event handler:
function handleClick() {
valueRef.current += 1;
}
However, if you expect the number displayed in the UI to change, this is not enough.
The UI will not automatically update because changing a ref does not trigger rendering.
When Should You Use a Ref?
Refs are useful when you need to retain information without making that information part of the rendered UI.
Common examples include:
DOM References
const inputRef = useRef(null);
Timer IDs
const timerRef = useRef(null);
Previous Values
const previousValueRef = useRef(value);
External Objects
const connectionRef = useRef(null);
Mutable Information
const renderCount = useRef(0);
The key question is:
Does this value need to cause the component to render when it changes?
If yes, state is usually the right choice.
If no, a ref may be appropriate.
DOM References
One of the most common uses of refs is accessing DOM elements.
For example:
import { useRef } from "react";
function SearchBox() {
const inputRef = useRef(null);
return (
<input
ref={inputRef}
type="text"
placeholder="Search"
/>
);
}
React assigns the actual DOM element to:
inputRef.current
After the element has been mounted, you can access it.
Focusing an Input
A classic use case is automatically focusing an input.
import { useRef } from "react";
function SearchBox() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>
Focus Input
</button>
</>
);
}
When the button is clicked:
inputRef.current.focus();
directly calls the DOM element’s focus() method.
This is an appropriate use of a ref because the operation is directly interacting with the browser DOM.
Automatically Focusing an Input
You can also focus an input when a component mounts.
import { useEffect, useRef } from "react";
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
Here, the ref gives access to the DOM node, while the Effect performs the focus operation after the element has been committed.
Scrolling to an Element
Refs can also be used for scrolling.
function Page() {
const sectionRef = useRef(null);
function scrollToSection() {
sectionRef.current.scrollIntoView({
behavior: "smooth",
});
}
return (
<>
<button onClick={scrollToSection}>
Go to Section
</button>
<div style={{ height: "100vh" }} />
<section ref={sectionRef}>
<h2>React Refs</h2>
</section>
</>
);
}
The ref points to the section, allowing the browser’s DOM API to scroll it into view.
Controlling a Video
Refs can be useful for controlling media elements.
function VideoPlayer() {
const videoRef = useRef(null);
function playVideo() {
videoRef.current.play();
}
function pauseVideo() {
videoRef.current.pause();
}
return (
<>
<video
ref={videoRef}
src="/videos/react-intro.mp4"
/>
<button onClick={playVideo}>
Play
</button>
<button onClick={pauseVideo}>
Pause
</button>
</>
);
}
The ref gives access to the browser’s HTML video element.
DOM Refs Should Be Used Carefully
Refs provide an escape hatch from React’s declarative approach.
For example, this is appropriate:
inputRef.current.focus();
But you generally should not use refs to manually change large parts of the UI.
Avoid using refs as a replacement for state:
ref.current = "dark";
when the value determines what should appear on screen.
If the UI depends on a value, state is generally more appropriate:
const [theme, setTheme] = useState("light");
Mutable Values
A ref can store a mutable value.
For example:
const valueRef = useRef(0);
You can change it:
valueRef.current = valueRef.current + 1;
The value remains available during future renders.
Consider:
function Example() {
const renderCount = useRef(0);
renderCount.current += 1;
return <p>Component rendered</p>;
}
The ref persists between renders.
However, you should be careful about modifying refs during rendering. A ref should generally be read or changed in event handlers, Effects, or other appropriate non-rendering code. Avoid using ref mutation as an uncontrolled way to influence what gets rendered.
Why Are Refs Persistent?
Consider:
const valueRef = useRef(0);
React keeps the same ref object between renders.
Conceptually:
First render
↓
ref object created
↓
Second render
↓
same ref object
↓
Third render
↓
same ref object
The value stored in:
ref.current
survives those renders.
Refs and Component Re-Renders
Suppose:
const valueRef = useRef(0);
and:
function handleClick() {
valueRef.current += 1;
}
Clicking the button changes the ref.
But React does not automatically render:
valueRef.current
again.
For example:
function Counter() {
const countRef = useRef(0);
function increment() {
countRef.current += 1;
console.log(countRef.current);
}
return (
<button onClick={increment}>
Increase
</button>
);
}
The console will show the changing value, but the component’s visible UI does not automatically update.
If you need the number displayed on the screen to update, use state:
const [count, setCount] = useState(0);
Storing Previous Values
A common ref pattern is remembering a previous value.
For example:
function Example({ value }) {
const previousValueRef = useRef(value);
useEffect(() => {
previousValueRef.current = value;
}, [value]);
return (
<p>
Previous value: {previousValueRef.current}
</p>
);
}
The important idea is that the ref stores information from the previous render.
Creating a usePrevious Hook
You can turn this pattern into a custom Hook:
import { useEffect, useRef } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Then:
function Price({ price }) {
const previousPrice = usePrevious(price);
return (
<div>
<p>Current price: ₹{price}</p>
<p>Previous price: ₹{previousPrice}</p>
</div>
);
}
When price changes, the Hook allows the component to compare the current value with the value stored from the previous render.
Previous Props
The same technique can be used for previous props.
function Product({ productId }) {
const previousProductId = usePrevious(productId);
return (
<p>
Previous product: {previousProductId}
</p>
);
}
Refs are useful here because the value needs to persist but does not itself need to trigger a render.
Previous State
You can also store a previous state value:
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
The state causes the component to render.
The ref remembers the value from the previous render.
Timers With Refs
Refs are particularly useful for storing timer IDs.
Consider a timeout:
const timeoutRef = useRef(null);
You can start it:
timeoutRef.current = setTimeout(() => {
console.log("Finished");
}, 3000);
And later clear it:
clearTimeout(timeoutRef.current);
A Complete Timer Example
import { useRef } from "react";
function Timer() {
const timerRef = useRef(null);
function startTimer() {
timerRef.current = setTimeout(() => {
console.log("Timer finished");
}, 3000);
}
function stopTimer() {
clearTimeout(timerRef.current);
}
return (
<>
<button onClick={startTimer}>
Start
</button>
<button onClick={stopTimer}>
Stop
</button>
</>
);
}
The timer ID needs to persist between event-handler calls, but it does not need to appear in the UI.
That makes a ref a good fit.
Intervals With Refs
The same idea works with setInterval.
function Timer() {
const intervalRef = useRef(null);
function start() {
intervalRef.current = setInterval(() => {
console.log("Running...");
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
The ref stores the interval ID.
Combining Timer Refs With Cleanup
If a component starts an interval, it should also clean it up when the component is removed.
import { useEffect, useRef } from "react";
function Timer() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log("Running...");
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
return <p>Timer running...</p>;
}
Here:
useRefstores the timer ID.useEffectstarts the timer.- The cleanup function stops it.
Callback Refs
A callback ref is a function passed to the ref attribute.
Instead of:
const inputRef = useRef(null);
<input ref={inputRef} />
you can write:
<input
ref={(node) => {
console.log(node);
}}
/>
React calls the function with the DOM node when it becomes available.
When the node is removed or the ref changes, React can call the callback with null.
Basic Callback Ref Example
function Input() {
const handleRef = (node) => {
if (node) {
node.focus();
}
};
return <input ref={handleRef} />;
}
When the input is attached, the callback receives the DOM element.
Why Use Callback Refs?
Callback refs are useful when you need to perform an action when a DOM node is attached or detached.
For example, they can be useful for:
- Measuring DOM elements
- Integrating with third-party libraries
- Managing dynamic elements
- Handling DOM nodes that appear conditionally
Measuring an Element
For example:
function Box() {
const handleRef = (node) => {
if (node) {
console.log(node.getBoundingClientRect());
}
};
return (
<div ref={handleRef}>
Web4Learners
</div>
);
}
When the element becomes available, the callback can inspect its dimensions.
For more complex measurement and layout synchronization, useLayoutEffect may sometimes be appropriate, but it should be used only when the timing of DOM measurement requires it.
Callback Refs With Dynamic Lists
Callback refs can also be useful when you need references to dynamically generated DOM elements.
For example:
function List({ items }) {
const nodes = useRef(new Map());
return (
<ul>
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
if (node) {
nodes.current.set(item.id, node);
} else {
nodes.current.delete(item.id);
}
}}
>
{item.name}
</li>
))}
</ul>
);
}
The Map stored inside the ref allows the component to associate item IDs with DOM nodes.
useImperativeHandle
Sometimes a parent component needs to call a specific method on a child component.
React provides useImperativeHandle for customizing the value exposed through a ref.
Import it:
import { useImperativeHandle, useRef } from "react";
The Hook is used to expose a controlled imperative API.
Why useImperativeHandle Exists
Suppose a custom input component internally contains an <input>.
A parent may need to:
- Focus the input
- Select its text
- Clear the input
Instead of exposing the entire internal DOM structure, the child can expose only the methods the parent needs.
Conceptually:
Parent
↓ ref
CustomInput
↓
Internal input
The child decides what the parent can access.
useImperativeHandle Syntax
A typical pattern is:
useImperativeHandle(ref, () => {
return {
// methods to expose
};
});
The first argument is the ref received by the component.
The second argument returns the value that should be exposed.
Exposing a Focus Method
With modern React, ref can be received as a prop.
import {
useImperativeHandle,
useRef,
} from "react";
function CustomInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
}
A parent can create a ref:
function Form() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<>
<CustomInput ref={inputRef} />
<button onClick={focusInput}>
Focus Input
</button>
</>
);
}
The parent does not need to know about the child’s internal inputRef.
It only knows that the exposed object has a:
focus()
method.
Exposing Multiple Methods
You can expose more than one method.
function CustomInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
},
select() {
inputRef.current.select();
},
}));
return <input ref={inputRef} />;
}
The parent can now use:
inputRef.current.focus();
inputRef.current.clear();
inputRef.current.select();
This creates a small imperative API for the component.
Keep Imperative Handles Small
useImperativeHandle should not be used to expose everything about a component.
Prefer exposing only the operations the parent genuinely needs.
For example:
{
focus()
}
is often better than exposing the entire internal DOM implementation.
This keeps the child component encapsulated.
useImperativeHandle Should Be Used Sparingly
React encourages declarative data flow.
Normally, a parent controls a child through props:
<Modal open={isOpen} />
This is declarative.
An imperative API looks like:
modalRef.current.open();
Imperative APIs can be useful for specific behaviors such as:
- Focus
- Scroll
- Animation controls
- Media controls
- Integrating with non-React systems
But they should not replace normal props and state for ordinary application logic.
Ref as a Prop
In modern React, function components can receive ref as a prop.
For example:
function MyInput({ ref }) {
return <input ref={ref} />;
}
A parent can use:
function App() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
}
The parent can then access:
inputRef.current
after the input has been committed.
This modern approach simplifies ref forwarding for function components.
Ref Props in Modern React
Historically, React commonly used forwardRef to pass refs through function components.
For example, older code may look like:
const MyInput = forwardRef(function MyInput(props, ref) {
return <input ref={ref} />;
});
Modern React allows ref to be received as a prop by function components, so new code can use:
function MyInput({ ref }) {
return <input ref={ref} />;
}
When working on existing projects, you may still encounter forwardRef, so it is useful to recognize the pattern.
Ref as a Prop With useImperativeHandle
The two concepts work together.
Child:
function SearchInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
}
Parent:
function SearchPage() {
const searchRef = useRef(null);
return (
<>
<SearchInput ref={searchRef} />
<button onClick={() => searchRef.current.focus()}>
Focus Search
</button>
</>
);
}
The parent receives only the API exposed by the child.
Refs and State Together
Refs and state can sometimes be used together.
For example:
function Counter() {
const [count, setCount] = useState(0);
const lastCountRef = useRef(count);
function handleClick() {
lastCountRef.current = count;
setCount(count + 1);
}
return (
<div>
<p>Current: {count}</p>
<p>Last stored value: {lastCountRef.current}</p>
<button onClick={handleClick}>
Increase
</button>
</div>
);
}
State controls what the UI displays.
The ref stores additional information that does not independently trigger rendering.
Common Ref Mistakes
Mistake: Using a Ref When the UI Needs to Update
Avoid:
const countRef = useRef(0);
function increment() {
countRef.current += 1;
}
if the UI needs to display the updated count.
Use state instead:
const [count, setCount] = useState(0);
Mistake: Expecting Ref Changes to Trigger Rendering
This:
ref.current = "new value";
does not cause a re-render.
If you need the screen to update, use state.
Mistake: Using Refs for Normal Data Flow
Do not use refs as a hidden replacement for props.
For example, if a parent needs to tell a child what to display, use:
<Profile user={user} />
rather than trying to communicate through a ref.
Mistake: Forgetting to Check for null
A DOM ref can be null before the element is mounted or after it is removed.
For example:
inputRef.current?.focus();
can safely call focus() only when the element exists.
Mistake: Manipulating the DOM Unnecessarily
React should generally control the rendered UI.
Use refs for specific imperative interactions rather than manually changing the DOM everywhere.
Mistake: Exposing Too Much Through useImperativeHandle
Avoid exposing a large internal API.
Expose only the methods that consumers actually need.
Mistake: Storing Rendered Information Only in a Ref
If the value affects what the user sees, storing it only in a ref will not update the UI.
Use state when the value is part of the rendered output.
Refs and Effects
Refs and Effects often work together.
For example:
function Input() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
Here:
useRefidentifies the DOM element.useEffectperforms the synchronization after the element is available.
This combination appears frequently in real React applications.
Refs and Event Handlers
Refs are also commonly used directly inside event handlers.
function SearchBox() {
const inputRef = useRef(null);
function handleSearch() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleSearch}>
Search
</button>
</>
);
}
Because the action is caused by a user interaction, there is no need for an Effect.
Refs and Third-Party Libraries
Some JavaScript libraries expect a real DOM element.
For example:
function Chart() {
const containerRef = useRef(null);
useEffect(() => {
const chart = createChart(containerRef.current);
return () => {
chart.destroy();
};
}, []);
return <div ref={containerRef} />;
}
The ref provides the DOM element.
The Effect establishes and cleans up the relationship with the external library.
This is a good example of React interacting with a system outside React.
Refs and Animation
Refs can also be used for imperative animation APIs.
For example:
function Box() {
const boxRef = useRef(null);
function moveBox() {
boxRef.current.animate(
[
{ transform: "translateX(0)" },
{ transform: "translateX(200px)" },
],
{
duration: 500,
fill: "forwards",
}
);
}
return (
<>
<button onClick={moveBox}>
Move
</button>
<div ref={boxRef}>
Box
</div>
</>
);
}
The ref provides access to the DOM element and its browser APIs.
A Practical Ref Decision Guide
When deciding between state and a ref, ask:
Does the value appear in the UI?
If yes, use state in most cases.
const [name, setName] = useState("");
Does changing the value need to cause a render?
If yes, use state.
Does the value need to persist between renders but should not cause a render?
A ref may be appropriate.
const timerRef = useRef(null);
Do you need to interact directly with a DOM element?
Use a ref.
const inputRef = useRef(null);
Do you need to expose a small imperative API from a child?
Consider useImperativeHandle.
Refs Cheat Sheet
| Concept | Purpose |
|---|---|
useRef | Stores a value that persists between renders |
.current | Holds the current ref value |
| DOM ref | Access a DOM element |
| Mutable ref | Store changing information without re-rendering |
| Previous value | Remember information from an earlier render |
| Timer ref | Store timeout or interval IDs |
| Callback ref | Run logic when a DOM node is attached or detached |
useImperativeHandle | Customize what a ref exposes |
| Ref as a prop | Pass a ref to a function component |
| State | Use when changes should update the UI |
Practice Exercise
Create a Web4Learners Search Input component.
The component should:
- Contain a search input
- Focus the input when the user clicks a button
- Provide a clear button
- Use
useReffor the DOM element - Avoid using state for the DOM reference itself
- Add a timer that automatically focuses the input after a short delay
- Clean up the timer when necessary
Start with:
import {
useEffect,
useRef,
} from "react";
function SearchInput() {
const inputRef = useRef(null);
const timerRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
function clearInput() {
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.focus();
}
}
useEffect(() => {
timerRef.current = setTimeout(() => {
inputRef.current?.focus();
}, 1000);
return () => {
clearTimeout(timerRef.current);
};
}, []);
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="Search Web4Learners"
/>
<button onClick={focusInput}>
Focus
</button>
<button onClick={clearInput}>
Clear
</button>
</div>
);
}
export default SearchInput;
This exercise combines several concepts from this chapter:
useRef
↓
DOM reference
↓
Event handlers
↓
Timer reference
↓
useEffect
↓
Cleanup
Key Takeaways
useRefcreates a mutable value that persists between renders.- A ref stores its value in the
.currentproperty. - Changing
.currentdoes not cause a re-render. - Use state when a changing value should update the UI.
- Use refs when information needs to persist without independently triggering rendering.
- Refs are commonly used to access DOM elements.
- DOM refs can be used for actions such as focusing, scrolling, and controlling media.
- Timer IDs are good candidates for refs because they need to persist between event-handler calls.
- Previous values can be stored in refs.
- Callback refs allow you to run logic when a DOM node is attached or detached.
useImperativeHandlelets a component customize what it exposes through a ref.- Modern React allows function components to receive
refas a prop. forwardRefis still important to recognize when working with existing React code.- Refs are an escape hatch for imperative interactions.
- Refs should not replace normal props and state.
- If the UI needs to respond to a value change, state is usually the correct choice.
- If you create timers, subscriptions, or external resources, make sure they are cleaned up appropriately.
- Keep imperative APIs small and expose only what a parent genuinely needs.
The most important idea to remember is:
Use refs when you need to remember something between renders or interact with something imperatively, without making that value part of React’s rendered state.