useRef
useRef in React covers persistent mutable values, DOM references, previous values, third-party instances, and values that should persist without triggering renders.
useRef is a React Hook that lets a component retain a mutable value between renders without causing a re-render when that value changes.
It is especially useful for:
- Accessing DOM elements
- Focusing an input
- Storing timer IDs
- Keeping references to external resources
- Remembering a previous value
- Storing mutable values that do not belong in rendered UI
- Integrating React with browser APIs and third-party libraries
The basic pattern is:
const ref = useRef(initialValue);
The value is available through:
ref.current
A useful mental model is:
useState↓Value changes → React renders againuseRef↓Value changes → React does not render again
Importing useRef
Import useRef from React:
import { useRef } from "react";
It can be used alongside other Hooks:
import { useEffect, useRef, useState } from "react";
Basic Syntax
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 update it:
countRef.current = 10;
Unlike state, changing ref.current does not trigger a re-render.
useRef Creates a Persistent Reference
A ref persists across renders.
For example:
function Example() {const valueRef = useRef(0);return (<button onClick={() => {valueRef.current += 1;console.log(valueRef.current);}}>Increase</button>);}
The value can change:
0 → 1 → 2 → 3 → 4
and the ref remains available across renders.
However, changing the ref itself does not cause React to render the component again.
useRef vs Normal Variables
Consider:
function Component() {let value = 0;function handleClick() {value++;console.log(value);}// ...}
A normal local variable is recreated when the component function executes again.
A ref persists:
function Component() {const valueRef = useRef(0);function handleClick() {valueRef.current++;console.log(valueRef.current);}// ...}
Mental model:
Normal variable↓Component renders↓Variable created↓Component renders again↓Variable created again
Versus:
useRef↓Initial ref created↓Component renders↓Component renders again↓Same ref object is retained
useRef vs useState
This is one of the most important distinctions in React.
useState
const [count, setCount] = useState(0);
Updating:
setCount(1);
can cause React to render again.
useRef
const countRef = useRef(0);
Updating:
countRef.current = 1;
does not cause React to render again.
Comparison:
| Feature | useState | useRef |
|---|---|---|
| Persists between renders | Yes | Yes |
| Updating value triggers render | Yes | No |
| Access value | Direct variable | .current |
| Intended for rendered UI | Yes | Usually no |
| Can store DOM reference | No | Yes |
| Mutable | Updated through setter | current can be changed |
A practical rule:
If changing the value should update the UI, use state. If the value needs to persist but changing it should not update the UI, a ref may be appropriate.
The Most Common useRef Use Case: DOM Access
Refs are commonly used to access DOM elements.
Example:
import { useRef } from "react";function SearchInput() {const inputRef = useRef(null);function focusInput() {inputRef.current?.focus();}return (<div><input ref={inputRef} /><button onClick={focusInput}>Focus Input</button></div>);}export default SearchInput;
The connection is:
<input>↓ref={inputRef}↓inputRef.current↓DOM element
When the input is mounted, React can assign the DOM element to:
inputRef.current
Why Does useRef Start With null?
For DOM refs, the initial value is often:
const inputRef = useRef(null);
Before the element is mounted:
inputRef.current = null
After React attaches the ref:
inputRef.current = <input DOM node>
When the element is removed, React can reset the ref.
Because the element may not exist at every point in the component lifecycle, optional chaining is often useful:
inputRef.current?.focus();
Accessing the DOM Safely
This is generally appropriate:
function Input() {const inputRef = useRef(null);function focusInput() {inputRef.current?.focus();}return (<><input ref={inputRef} /><button onClick={focusInput}>Focus</button></>);}
Avoid trying to access:
inputRef.current
during rendering when the DOM element has not been mounted yet.
The ref is normally available after React has committed the DOM.
useRef With useEffect
Refs and effects are frequently used together.
import { useEffect, useRef } from "react";function AutoFocusInput() {const inputRef = useRef(null);useEffect(() => {inputRef.current?.focus();}, []);return <input ref={inputRef} />;}
The sequence is:
Render↓DOM is committed↓inputRef.current points to input↓Effect runs↓focus()
This is a common React pattern for interacting with the DOM after it exists.
useRef for Timer IDs
Refs can store timer IDs.
import { useRef } from "react";function Timer() {const timerRef = useRef(null);function startTimer() {timerRef.current = setInterval(() => {console.log("Tick");}, 1000);}function stopTimer() {if (timerRef.current !== null) {clearInterval(timerRef.current);timerRef.current = null;}}return (<div><button onClick={startTimer}>Start</button><button onClick={stopTimer}>Stop</button></div>);}
Why use a ref?
Because the timer ID needs to persist between renders, but changing it does not need to update the UI.
Cleaning Up Timer Resources
If an interval should be cleaned up when the component unmounts:
import { useEffect, useRef } from "react";function Timer() {const timerRef = useRef(null);useEffect(() => {timerRef.current = setInterval(() => {console.log("Tick");}, 1000);return () => {if (timerRef.current !== null) {clearInterval(timerRef.current);}};}, []);return <p>Timer running</p>;}
The ref stores the resource identifier while the effect manages the lifecycle.
useRef for Previous Values
A ref can be used to remember a value from a previous render.
For example:
import { useEffect, useRef } from "react";function User({ userId }) {const previousUserId = useRef(null);useEffect(() => {previousUserId.current = userId;}, [userId]);return (<p>Previous user: {previousUserId.current}</p>);}
The idea is:
Render 1previousUserId = nulluserId = 10EffectpreviousUserId.current = 10Render 2previousUserId = 10userId = 20
A ref provides persistent storage without making the stored value itself part of the rendered state update cycle.
A Better Previous Value Example
A reusable Hook can encapsulate the pattern:
import { useEffect, useRef } from "react";function usePrevious(value) {const ref = useRef();useEffect(() => {ref.current = value;}, [value]);return ref.current;}
Usage:
function Counter() {const [count, setCount] = useState(0);const previousCount = usePrevious(count);return (<div><p>Current: {count}</p><p>Previous: {previousCount}</p><buttononClick={() =>setCount(previous => previous + 1)}>Increase</button></div>);}
This introduces the idea of custom Hooks, which will be explored later in the course.
useRef for Mutable Values
Refs can store mutable values:
const requestIdRef = useRef(null);
requestIdRef.current = requestId;
The important characteristic is:
Value persists+Value can change+Changing it does not trigger render
This makes refs useful for information that belongs to the component's operational logic rather than its visual output.
useRef Does Not Mean "DOM Only"
A common beginner misconception is:
useRef is only for DOM elements.
That is not true.
Refs can store:
DOM nodesTimer IDsPrevious valuesExternal objectsInstance-like valuesRequest identifiersMutable flagsThird-party library instances
DOM access is simply the most visible use case.
useRef and Third-Party Libraries
Suppose a third-party chart library needs a DOM container:
function Chart() {const chartContainerRef = useRef(null);useEffect(() => {if (!chartContainerRef.current) {return;}const chart = createChart(chartContainerRef.current);return () => {chart.destroy();};}, []);return <div ref={chartContainerRef} />;}
Here:
useRef↓Access DOM containeruseEffect↓Create external chartcleanup↓Destroy chart
This is a powerful integration pattern.
useRef and External Objects
A ref can also retain an external object:
const connectionRef = useRef(null);
For example:
useEffect(() => {const connection = createConnection(roomId);connectionRef.current = connection;connection.connect();return () => {connection.disconnect();connectionRef.current = null;};}, [roomId]);
The ref gives the component a persistent reference to the external resource.
useRef and Closures
React function components use JavaScript closures.
A ref provides a mutable object whose current property can be read by code that has access to that ref.
For example:
const valueRef = useRef(0);function handleClick() {valueRef.current += 1;}
The same ref object persists across renders, so event handlers and effects can access its current value.
This differs from state snapshots, where each render has its own state values.
State Snapshot vs Ref Value
Consider:
const [count, setCount] = useState(0);const countRef = useRef(0);
State behaves like a render snapshot:
Render Acount = 0
A state update requests a new render:
setCount(1);
The current render still has:
count = 0
A ref is different:
countRef.current = 1;
The ref object's current property is mutable.
Conceptually:
State↓Snapshot associated with renderRef↓Persistent mutable object
This difference is important when reasoning about asynchronous callbacks and effects.
useRef Does Not Trigger Rendering
Consider:
function Example() {const countRef = useRef(0);function increment() {countRef.current += 1;}return (<div><p>{countRef.current}</p><button onClick={increment}>Increment</button></div>);}
Clicking the button changes:
countRef.current
but React does not automatically render again.
Therefore, the displayed <p> may not update.
If the value needs to appear dynamically in the UI, use state:
const [count, setCount] = useState(0);
This is a critical rule:
Do not use a ref for data that must drive rendered UI.
A Common Mistake
Incorrect:
const countRef = useRef(0);function increment() {countRef.current++;}
when the component needs to display:
<p>{countRef.current}</p>
Use:
const [count, setCount] = useState(0);function increment() {setCount(previous => previous + 1);}
The decision is based on whether the value affects rendering.
useRef and DOM Manipulation
React encourages declarative UI:
return <button disabled={isDisabled}>Save</button>;
However, some imperative DOM operations are appropriate:
inputRef.current?.focus();
videoRef.current?.play();
elementRef.current?.scrollIntoView();
Refs provide a controlled escape hatch for these operations.
Use them when you need direct access to a DOM node or external object, not as a replacement for React's normal rendering model.
Focus Management Example
import { useRef } from "react";function LoginForm() {const emailRef = useRef(null);function focusEmail() {emailRef.current?.focus();}return (<form><inputref={emailRef}type="email"placeholder="Email"/><buttontype="button"onClick={focusEmail}>Focus Email</button></form>);}export default LoginForm;
Scroll Management Example
import { useRef } from "react";function MessageList() {const bottomRef = useRef(null);function scrollToBottom() {bottomRef.current?.scrollIntoView({behavior: "smooth"});}return (<div><div><p>Message 1</p><p>Message 2</p><p>Message 3</p><div ref={bottomRef} /></div><button onClick={scrollToBottom}>Scroll to Bottom</button></div>);}export default MessageList;
The ref points to a DOM element that can be used as a scrolling target.
Video Control Example
import { useRef } from "react";function VideoPlayer() {const videoRef = useRef(null);function playVideo() {videoRef.current?.play();}function pauseVideo() {videoRef.current?.pause();}return (<div><videoref={videoRef}src="/video.mp4"controls/><button onClick={playVideo}>Play</button><button onClick={pauseVideo}>Pause</button></div>);}
The video element is managed by the browser, while React uses the ref to access it.
useRef and Conditional Rendering
A ref may be null when the referenced element is not currently rendered.
function Modal({ isOpen }) {const dialogRef = useRef(null);// dialogRef.current may be null// when the modal is not renderedreturn (<>{isOpen && (<dialog ref={dialogRef}>Modal content</dialog>)}</>);}
Always consider whether the referenced element exists before accessing it:
dialogRef.current?.focus();
Callback Refs
React also supports callback refs.
Instead of:
const inputRef = useRef(null);return <input ref={inputRef} />;
you can use:
<inputref={node => {// node is the DOM element}}/>
Callback refs are useful when ref attachment itself needs custom behavior.
However, useRef is usually simpler when you only need to retain a DOM reference.
useRef and Initial Values
The initial value can be anything:
const numberRef = useRef(0);
const textRef = useRef("");
const objectRef = useRef({});
const elementRef = useRef(null);
The most common DOM pattern is:
const elementRef = useRef(null);
because the element does not exist until React mounts it.
Ref Values Are Mutable
You can assign:
ref.current = newValue;
For example:
const statusRef = useRef("idle");statusRef.current = "loading";
But remember:
Mutation↓No automatic render
If the UI needs to show "loading":
const [status, setStatus] = useState("idle");
is usually more appropriate.
useRef and Initialization
A ref object is created as part of the component's Hook state and remains stable across renders.
For example:
const inputRef = useRef(null);
You can think of it as:
Component instance│└── inputRef│└── current
On future renders, React preserves the ref object.
Avoid Writing Refs During Rendering
Although refs are mutable, avoid using them as an ordinary mechanism for changing values during rendering.
Prefer:
function Component() {const valueRef = useRef(0);function handleClick() {valueRef.current += 1;}return <button onClick={handleClick}>Click</button>;}
rather than using render-time mutation to coordinate application behavior.
Rendering should remain predictable and free from unnecessary side effects.
useRef and Strict Mode
Development Strict Mode can expose mistakes in component logic and effect setup.
Refs themselves do not cause renders, but code that initializes external resources through refs still needs correct lifecycle management.
For example:
useEffect(() => {const connection = createConnection();connectionRef.current = connection;connection.connect();return () => {connection.disconnect();connectionRef.current = null;};}, []);
The cleanup makes the external resource lifecycle explicit.
Common useRef Mistakes
Mistake 1: Using Ref When UI Must Update
Incorrect:
const countRef = useRef(0);countRef.current++;
if the updated count needs to be displayed.
Use state instead:
const [count, setCount] = useState(0);
Mistake 2: Forgetting .current
Incorrect:
inputRef.focus();
Correct:
inputRef.current?.focus();
Mistake 3: Accessing a Null DOM Ref
Incorrect:
inputRef.current.focus();
when the element may not exist.
Safer:
inputRef.current?.focus();
Mistake 4: Using Refs as Global State
Refs belong to a component instance.
Do not treat:
const ref = useRef();
as a global application state solution.
For shared application state, use appropriate React architecture such as lifted state, Context, or another state-management approach when justified.
Mistake 5: Using Refs to Avoid Correct State Design
A ref should not be used simply because state updates seem inconvenient.
Ask:
Does this value affect the UI?
If yes, state is often the correct choice.
Mistake 6: Forgetting Cleanup for Resources
If a ref stores a timer, connection, subscription, or third-party instance, make sure the resource lifecycle is managed correctly.
When Should You Use useRef?
Use useRef when you need:
1. A DOM reference
const inputRef = useRef(null);
2. A persistent mutable value
const timerRef = useRef(null);
3. A previous value
const previousValueRef = useRef();
4. A third-party instance
const chartRef = useRef(null);
5. A value that should persist without triggering rendering
const requestIdRef = useRef(null);
When Should You Use useState Instead?
Use state when the value:
- Appears in the rendered UI
- Determines what the component renders
- Needs to trigger a render when changed
- Represents interactive application state
For example:
const [isOpen, setIsOpen] = useState(false);
because changing isOpen should change the UI.
Decision Table
| Question | Use |
|---|---|
| Should changing the value update the UI? | useState |
| Need a DOM element reference? | useRef |
| Need a timer ID between renders? | useRef |
| Need previous value storage? | useRef |
| Need a selected tab displayed in UI? | useState |
| Need to retain a third-party instance? | useRef |
| Need fetched data displayed in UI? | useState |
| Need an external connection reference? | useRef |
useRef + useState Example
Sometimes both Hooks are appropriate.
import { useRef, useState } from "react";function Stopwatch() {const [seconds, setSeconds] = useState(0);const intervalRef = useRef(null);function start() {if (intervalRef.current !== null) {return;}intervalRef.current = setInterval(() => {setSeconds(previous => previous + 1);}, 1000);}function stop() {if (intervalRef.current !== null) {clearInterval(intervalRef.current);intervalRef.current = null;}}return (<div><p>{seconds}s</p><button onClick={start}>Start</button><button onClick={stop}>Stop</button></div>);}
Here:
seconds↓useState↓Displayed in UIinterval ID↓useRef↓Operational value
This is a very useful real-world pattern.
useRef + useEffect Example
Another common combination is:
import { useEffect, useRef } from "react";function Example() {const inputRef = useRef(null);useEffect(() => {inputRef.current?.focus();}, []);return <input ref={inputRef} />;}
The responsibilities are clear:
useRef↓Stores DOM referenceuseEffect↓Runs synchronization after mount
useRef + Previous State Example
import { useEffect, useRef, useState } from "react";function Counter() {const [count, setCount] = useState(0);const previousCountRef = useRef();useEffect(() => {previousCountRef.current = count;}, [count]);return (<div><p>Current: {count}</p><p>Previous: {previousCountRef.current}</p><buttononClick={() =>setCount(previous => previous + 1)}>Increase</button></div>);}
The ref stores information between renders without becoming another piece of state.
Practical Example: Prevent Duplicate Actions
A ref can sometimes track an operational flag:
function SubmitButton() {const submittingRef = useRef(false);async function handleSubmit() {if (submittingRef.current) {return;}submittingRef.current = true;try {await submitForm();} finally {submittingRef.current = false;}}return (<button onClick={handleSubmit}>Submit</button>);}
However, if the UI needs to display a loading state, use state as well:
const [isSubmitting, setIsSubmitting] = useState(false);
A ref is appropriate for an internal operational value; state is appropriate for UI state.
Practical Example: Tracking Render Count
A ref can be used for diagnostics:
function DebugComponent() {const renderCount = useRef(0);renderCount.current += 1;return (<p>Render count: {renderCount.current}</p>);}
This is useful for learning and debugging, but be careful about mutating refs during render in production application logic. Prefer refs for stable operational data and debugging scenarios rather than using them to drive application behavior.
useRef and Performance
Because changing a ref does not trigger rendering, refs can avoid unnecessary renders for values that do not belong in the UI.
However, this does not mean:
useRef is a faster version of useState.
They have different purposes.
Do not replace state with refs merely to reduce renders.
Correct design comes first:
UI data↓useStateNon-UI persistent value↓useRef
useRef and Component Architecture
Refs are usually local to the component that owns them.
If a parent needs to interact with a child's DOM or imperative behavior, React provides patterns such as passing refs through component boundaries and, in appropriate cases, exposing imperative methods.
These advanced patterns should be used sparingly because declarative props are generally easier to reason about.
Best Practices
1. Use State for Rendered Data
If the value affects JSX:
useState(...)
is usually the correct choice.
2. Use Refs for Persistent Non-UI Values
Examples:
Timer IDsDOM nodesPrevious valuesExternal instancesRequest identifiers
3. Use .current
Remember:
ref.current
not:
ref
when accessing the stored value.
4. Check for Null DOM Refs
Prefer:
inputRef.current?.focus();
when the element may not exist.
5. Pair Resource Refs With Cleanup
If a ref stores a resource:
connectionRef.current
make sure the corresponding resource is correctly cleaned up.
6. Do Not Treat Refs as Global State
A ref belongs to a particular component instance.
7. Do Not Use Refs to Bypass React's Rendering Model
Refs are an escape hatch, not a replacement for declarative state and props.
Interview questions & answers
Interview preparation
10 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is useRef?
BeginnerModel answer
useRefis a React Hook that provides a persistent mutable reference whose.currentvalue can be changed without causing a re-render.2What is the most common use of useRef?
BeginnerModel answer
Accessing DOM elements:const inputRef = useRef(null); → <input ref={inputRef} />3Why is useRef initialized with null for DOM elements?
BeginnerModel answer
Because the DOM element does not exist before React mounts it.
4Why are refs useful with timers?
BeginnerModel answer
A timer ID needs to persist between renders so that it can later be cancelled, but changing the timer ID usually does not need to update the UI.
Intermediate
5What does useRef return?
IntermediateModel answer
It returns an object with acurrentproperty:const ref = useRef(initialValue);Conceptually:{ → current: initialValue → }6What is the difference between useRef and useState?
IntermediateModel answer
Both persist values between renders. The key difference is that updating state can trigger a render, while updatingref.currentdoes not.7Can a ref be used to update UI?
IntermediateModel answer
Not by itself. Changing:ref.currentdoes not trigger a render. Use state if the UI needs to update.
Advanced
8Does changing ref.current trigger a render?
AdvancedModel answer
No.ref.current = newValue;does not itself trigger a React render.9Can useRef store values other than DOM nodes?
AdvancedModel answer
Yes. It can store:- Timer IDs
- Previous values
- External instances
- Mutable operational values
- Request identifiers
- DOM nodes
10Can useRef replace state?
AdvancedModel answer
No.
They solve different problems.
Use state for data that drives rendering and refs for persistent values that do not need to trigger rendering.
Practice Exercises
Exercise 1: Auto-Focus
Create a component with:
- An input
- A button
- A ref attached to the input
- A button that focuses the input
Exercise 2: Scroll to Bottom
Create a message list where:
- A ref points to the bottom element.
- Clicking a button scrolls to the bottom.
Exercise 3: Previous Value
Create a component that:
- Uses state for a counter.
- Uses a ref to remember the previous counter value.
- Displays current and previous values.
Exercise 4: Timer
Create a timer that:
- Uses state for elapsed seconds.
- Uses a ref for the interval ID.
- Has Start and Stop buttons.
- Cleans up the interval.
Exercise 5: Video Player
Create a custom video controller with:
- Play button
- Pause button
- Video element
- Ref attached to the video
Exercise 6: Window Resize
Create a component that:
- Uses state for the displayed window width.
- Uses an effect for the resize listener.
- Uses cleanup to remove the listener.
Then explain why the width belongs in state rather than a ref.
Mini Project: Stopwatch
Build a reusable stopwatch.
Requirements:
Display:00:00
Buttons:
StartStopReset
Use:
useState → elapsed timeuseRef → interval IDuseEffect → cleanup
A possible implementation:
import { useEffect, useRef, useState } from "react";function Stopwatch() {const [seconds, setSeconds] = useState(0);const intervalRef = useRef(null);function start() {if (intervalRef.current !== null) {return;}intervalRef.current = setInterval(() => {setSeconds(previous => previous + 1);}, 1000);}function stop() {if (intervalRef.current !== null) {clearInterval(intervalRef.current);intervalRef.current = null;}}function reset() {stop();setSeconds(0);}useEffect(() => {return () => {if (intervalRef.current !== null) {clearInterval(intervalRef.current);}};}, []);return (<div><h2>{seconds}s</h2><button onClick={start}>Start</button><button onClick={stop}>Stop</button><button onClick={reset}>Reset</button></div>);}export default Stopwatch;
The architecture is:
Stopwatch│┌─────────┴─────────┐│ │elapsed time interval ID│ │useState useRef│ │drives UI operational data│ │└─────────┬─────────┘│useEffect│cleanup
Quick Cheat Sheet
Import
import { useRef } from "react";
Create ref
const ref = useRef(null);
Read value
ref.current
Update value
ref.current = newValue;
DOM reference
const inputRef = useRef(null);<input ref={inputRef} />
Focus
inputRef.current?.focus();
Timer ID
const timerRef = useRef(null);
Previous value
const previousRef = useRef();useEffect(() => {previousRef.current = value;}, [value]);
State vs Ref
useState → changing UI datauseRef → persistent non-UI data