React Refs Explained — Accessing DOM Elements and Mutable Values
In this tutorial, you will learn about React Refs Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
React refs provide a way to access DOM elements directly and store mutable values that persist across renders without causing re-renders.
What You'll Learn
- What refs are and when to use them
- How to use useRef for DOM element access
- How to store mutable values without re-renders
- How to forward refs to child components
- How to use callback refs for dynamic refs
Why It Matters
Refs are the escape hatch from React's declarative world. They let you do imperative things like focus inputs, measure elements, and integrate with third-party libraries that need direct DOM access.
Real-World Use
Durga Antivirus Pro uses refs to focus the search input on navigation, measure chart container dimensions for responsive graphs, and store Websocket instances that should not trigger re-renders.
flowchart LR
A[Component] -->|useRef| B[Ref Object]
B --> C[DOM Element]
B --> D[Mutable Value]
C --> E[Focus / Measure / Animate]
D --> F[Persist Across Renders]
style A fill:#3b82f6,color:#fff
Accessing DOM Elements
The most common use for refs:
import { useRef, useEffect } from "react";
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input when the component mounts
inputRef.current.focus();
}, []);
return (
<div>
<label htmlFor="search">Search:</label>
<input
ref={inputRef}
id="search"
type="text"
placeholder="Type to search..."
/>
</div>
);
}
Expected output: When the component renders, the input field automatically receives focus. The cursor blinks inside it.
useRef returns a mutable object with a .current property. Initially set to the argument passed to useRef (null in this case). When the ref is attached to a DOM element via the ref attribute, React sets ref.current to that DOM node.
Mutable Values That Persist
Refs can store any value without causing re-renders:
import { useRef, useState } from "react";
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const startTimeRef = useRef(null);
const start = () => {
if (intervalRef.current) return; // Already running
startTimeRef.current = Date.now() - time * 1000;
intervalRef.current = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000);
setTime(elapsed);
}, 100);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const reset = () => {
stop();
setTime(0);
startTimeRef.current = null;
};
return (
<div>
<p>Elapsed: {time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Expected output: A stopwatch that starts, stops, and resets. The interval ID is stored in a ref and does not cause re-renders when changed.
Unlike state, updating a ref's .current property does NOT trigger a re-render. This makes refs ideal for storing timers, subscriptions, and other imperative handles.
forwardRef
Pass refs through to child components:
import { forwardRef, useRef } from "react";
const FancyInput = forwardRef((props, ref) => {
return (
<div className="fancy-input">
<label>{props.label}</label>
<input ref={ref} className="input" {...props} />
</div>
);
});
FancyInput.displayName = "FancyInput";
function ParentForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert(`Name: ${nameRef.current.value}, Email: ${emailRef.current.value}`);
};
const focusFirst = () => {
nameRef.current.focus();
};
return (
<form onSubmit={handleSubmit}>
<FancyInput ref={nameRef} label="Name" placeholder="Enter name" />
<FancyInput ref={emailRef} label="Email" type="email" placeholder="Enter email" />
<button type="button" onClick={focusFirst}>Focus Name</button>
<button type="submit">Submit</button>
</form>
);
}
Expected output: Two fancy inputs. Clicking "Focus Name" focuses the name field. Submit reads values from the refs.
forwardRef lets a component accept a ref prop and forward it to a DOM element inside it. Without it, the ref prop would not work on custom components.
Callback Refs
For dynamic ref management:
import { useState, useCallback } from "react";
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>
{show ? "Hide" : "Show"} box
</button>
{show && (
<div ref={measuredRef} style={{
padding: "20px",
background: "#3b82f6",
color: "white",
borderRadius: "8px"
}}>
<p>This box is {height}px tall</p>
</div>
)}
</div>
);
}
Expected output: When the box renders, its height is measured and displayed. When re-rendered (e.g., content changes), the callback fires again.
Callback refs give finer control. React calls the callback with the DOM node when it mounts and with null when it unmounts. This is useful for measuring, animations, and dynamic ref logic.
Managing Imperative Handles
Expose specific methods, not the whole DOM node:
import { forwardRef, useImperativeHandle, useRef } from "react";
const VideoPlayer = forwardRef(({ src }, ref) => {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
},
pause() {
videoRef.current.pause();
},
stop() {
videoRef.current.pause();
videoRef.current.currentTime = 0;
},
getDuration() {
return videoRef.current.duration;
},
}), []);
return (
<video ref={videoRef} src={src} width="400" controls />
);
});
VideoPlayer.displayName = "VideoPlayer";
function VideoController() {
const playerRef = useRef(null);
return (
<div>
<VideoPlayer ref={playerRef} src="https://example.com/video.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.pause()}>Pause</button>
<button onClick={() => playerRef.current.stop()}>Stop</button>
</div>
);
}
Expected output: Control buttons that play, pause, and stop the video through the exposed imperative handle.
useImperativeHandle lets you customize the instance value exposed to parent refs. Instead of exposing the entire DOM element, you expose only the methods you want.
Common Mistakes
Overusing refs when state would work — Use refs only for imperative operations. If you can achieve the same with state, do that.
Reading ref.current in render — Refs are not reactive. Reading
ref.currentduring render gives the value at render time, which may be stale.Setting refs in render — Setting
ref.currentduring render is a side effect and belongs in useEffect.Forgetting forwardRef for component refs — Custom components do not pass
refthrough unless wrapped inforwardRef.Refs as a replacement for state — Refs do not trigger re-renders. If the UI needs to update when the value changes, use state.
Practice Questions
What is a ref in React? A mutable object with a
.currentproperty that persists across renders without triggering re-renders.How do you access a DOM element with useRef? Pass the ref object's ref attribute to the JSX element:
<input ref={myRef} />.What does forwardRef do? It lets a custom component accept and forward a ref to one of its child DOM elements.
What is useImperativeHandle used for? To restrict the methods exposed when using forwardRef, so the parent can only call specific methods.
When should you use a callback ref instead of useRef? When you need to run code when the ref attaches or detaches, or when you need to measure a dynamic element.
Challenge
Build a DraggablePanel component that uses refs for the drag handle and panel elements. Implement drag logic using mouse events. Store the drag state (isDragging, offset) in refs to avoid unnecessary re-renders during drag. Only update state when the drag ends.
FAQ
Mini Project
Build a RichTextEditor component using a contentEditable div. Use a ref to access the div for formatting commands (bold, italic, underline). Store the history of edits in a ref to implement undo/redo. Measure the editor height with a callback ref and adjust the container. Use forwardRef to expose getContent and setContent methods to the parent.
What's Next
Continue with context and reducers:
React Context, React Reducers, React Hooks
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro