AiTechWorlds
AiTechWorlds
University learning companion
React & Next.js Complete Course · useRef & Forwarding Refs
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “useRef & Forwarding Refs” in the larger system. It complements—not replaces—a topic-specific worked diagram.
useRef is a hook that returns a mutable container { current: value } which persists across renders without ever triggering one when it changes.
Think of it as a sticky note on your monitor versus a value displayed on screen. The sticky note (a ref) holds information you can update anytime, but updating it doesn't redraw anything it's just there for you to check later. A value shown on screen (state) redraws the UI the moment it changes. useRef serves two distinct purposes built on this same idea: accessing DOM elements directly, and storing mutable values that persist between renders without causing re renders.
The most common use: getting a direct reference to a DOM node so you can call native DOM methods on it.
import { useRef, useEffect } from "react";
// Auto-focus an input on mount
function SearchModal({ isOpen }: { isOpen: boolean }) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
}
}, [isOpen]);
return (
<dialog open={isOpen}>
<input ref={inputRef} placeholder="Search..." />
</dialog>
);
}After the component mounts, inputRef.current holds the actual <input> DOM element. You can call any native DOM method on it: .focus(), .blur(), .scrollIntoView(), .select(), etc.
useRef returns a mutable object { current: initialValue }. Changing .current does NOT cause a re render this is the key difference from useState.
// Tracking the previous value of a prop
function PriceDisplay({ price }: { price: number }) {
const prevPriceRef = useRef<number>(price);
useEffect(() => {
prevPriceRef.current = price; // update after render
});
const prevPrice = prevPriceRef.current;
const changed = prevPrice !== price;
return (
<div>
<span className={changed ? (price > prevPrice ? "text-green-600" : "text-red-600") : ""}>
${price.toFixed(2)}
</span>
{changed && <small>(was ${prevPrice.toFixed(2)})</small>}
</div>
);
}// Tracking a timer ID
function Countdown() {
const [seconds, setSeconds] = useState(60);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start() {
timerRef.current = setInterval(() => {
setSeconds(s => {
if (s <= 1) {
clearInterval(timerRef.current!);
return 0;
}
return s - 1;
});
}, 1000);
}
function stop() {
clearInterval(timerRef.current!);
}
return (
<div>
<p>{seconds}s remaining</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}// Preventing the initial useEffect run (common pattern)
function DataFetcher({ userId }: { userId: string }) {
const [data, setData] = useState(null);
const isMountRef = useRef(false);
useEffect(() => {
if (!isMountRef.current) {
isMountRef.current = true;
return; // skip on first render
}
fetchUser(userId).then(setData);
}, [userId]);
// ...
}Store the timer ID in a ref so it persists between renders:
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value);
// Clear previous timer
if (debounceTimer.current) clearTimeout(debounceTimer.current);
// Set new timer
debounceTimer.current = setTimeout(() => {
onSearch(value);
}, 300);
}
// Clean up on unmount
useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
return <input value={query} onChange={handleChange} />;
}When you build a wrapper component around a native element, consumers might want a ref to the underlying DOM node. forwardRef enables this:
import { forwardRef } from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
// The ref is passed as the second argument
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, error, className = "", ...props },
ref // <-- the forwarded ref
) {
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
<input
ref={ref} // attach to the underlying DOM element
className={`w-full border rounded-lg px-3 py-2 ${error ? "border-red-400" : "border-gray-300"} ${className}`}
{...props}
/>
{error && <p className="text-red-600 text-sm mt-1">{error}</p>}
</div>
);
});
// Consumer can now use a ref:
function Form() {
const emailRef = useRef<HTMLInputElement>(null);
function focusEmail() {
emailRef.current?.focus();
}
return (
<form>
<Input ref={emailRef} label="Email" type="email" />
<button type="button" onClick={focusEmail}>Focus email</button>
</form>
);
}Sometimes you want to expose a custom API through a ref not the raw DOM node, but specific methods you control:
import { useRef, forwardRef, useImperativeHandle } from "react";
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (seconds: number) => void;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(function VideoPlayer(
{ src },
ref
) {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (seconds) => {
if (videoRef.current) videoRef.current.currentTime = seconds;
},
}), []);
return <video ref={videoRef} src={src} />;
});
// Consumer gets the custom API, not the raw DOM element
function App() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seek(30)}>Skip to 0:30</button>
</div>
);
}useRef vs useState:
- Both persist between renders
- useRef changes do NOT trigger re-renders
- useState changes DO trigger re-renders
- Use useRef for values that are implementation details
- Use useState for values that should be reflected in the UI
useRef vs a variable in the component body:
- Variables in the component body are recreated on every render
- useRef persists the same object across rendersNext lesson: Custom hooks extracting and reusing component logic.
University learning companion
React & Next.js Complete Course · useRef & Forwarding Refs
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises