|$ curl https://forge-ai.dev/api/markdown?path=docs/react/refs
$cat docs/react-—-refs-&-dom.md
updated Last week·15 min read·published

React — Refs & DOM

ReactIntermediate🎯Free Tools
Introduction

Refs provide a way to access DOM nodes or React elements created in the render method. They are an escape hatch from the declarative React model — use them when you need imperative access: focusing inputs, measuring dimensions, scrolling, or integrating with non-React libraries.

useRef
use_ref.tsx
TSX
1import { useRef, useEffect } from "react";
2
3// useRef returns a mutable object whose .current property persists
4// across renders without causing re-renders when changed
5
6function TextInputWithFocus() {
7 const inputRef = useRef<HTMLInputElement>(null);
8
9 function handleClick() {
10 // .current gives access to the DOM node
11 inputRef.current?.focus();
12 }
13
14 return (
15 <div>
16 <input ref={inputRef} type="text" />
17 <button onClick={handleClick}>Focus Input</button>
18 </div>
19 );
20}
21
22// useRef for storing previous values
23function usePrevious<T>(value: T): T | undefined {
24 const ref = useRef<T>();
25
26 useEffect(() => {
27 ref.current = value;
28 }, [value]);
29
30 return ref.current;
31}
32
33function Counter() {
34 const [count, setCount] = useState(0);
35 const prevCount = usePrevious(count);
36
37 return (
38 <p>
39 Now: {count}, Before: {prevCount ?? "N/A"}
40 </p>
41 );
42}
43
44// useRef for storing interval IDs
45function Timer() {
46 const [seconds, setSeconds] = useState(0);
47 const intervalRef = useRef<NodeJS.Timeout | null>(null);
48
49 function start() {
50 if (intervalRef.current) return;
51 intervalRef.current = setInterval(() => {
52 setSeconds((s) => s + 1);
53 }, 1000);
54 }
55
56 function stop() {
57 if (intervalRef.current) {
58 clearInterval(intervalRef.current);
59 intervalRef.current = null;
60 }
61 }
62
63 useEffect(() => {
64 return () => {
65 if (intervalRef.current) clearInterval(intervalRef.current);
66 };
67 }, []);
68
69 return (
70 <div>
71 <p>{seconds}s</p>
72 <button onClick={start}>Start</button>
73 <button onClick={stop}>Stop</button>
74 </div>
75 );
76}
77
78// useRef for storing render count
79function RenderCounter() {
80 const renderCount = useRef(0);
81
82 useEffect(() => {
83 renderCount.current += 1;
84 });
85
86 return <p>Rendered {renderCount.current} times</p>;
87}

info

Changing ref.current does not trigger a re-render. This makes refs perfect for storing mutable values that should persist across renders but should not affect the UI.
Ref Callbacks
ref_callbacks.tsx
TSX
1// Ref callback — function called with the DOM node on mount/unmount
2function MeasureExample() {
3 const [height, setHeight] = useState(0);
4
5 const measuredRef = useCallback((node: HTMLDivElement | null) => {
6 if (node !== null) {
7 setHeight(node.getBoundingClientRect().height);
8 }
9 }, []);
10
11 return (
12 <div>
13 <p ref={measuredRef}>Hello, world</p>
14 <p>The above paragraph is {Math.round(height)}px tall</p>
15 </div>
16 );
17}
18
19// Ref callback with cleanup — for subscription patterns
20function TrackDimensions() {
21 const [size, setSize] = useState({ width: 0, height: 0 });
22
23 const ref = useCallback((node: HTMLDivElement | null) => {
24 if (!node) return;
25
26 const observer = new ResizeObserver((entries) => {
27 for (const entry of entries) {
28 setSize({
29 width: entry.contentRect.width,
30 height: entry.contentRect.height,
31 });
32 }
33 });
34
35 observer.observe(node);
36
37 // Cleanup function — called when node unmounts
38 return () => observer.disconnect();
39 }, []);
40
41 return (
42 <div ref={ref}>
43 <p>Width: {size.width}px, Height: {size.height}px</p>
44 </div>
45 );
46}
47
48// Multiple refs with callback pattern
49function MultiRefForm() {
50 const refs = useRef<Map<string, HTMLInputElement>>(new Map());
51
52 const setRef = useCallback(
53 (name: string) => (node: HTMLInputElement | null) => {
54 if (node) {
55 refs.current.set(name, node);
56 } else {
57 refs.current.delete(name);
58 }
59 },
60 []
61 );
62
63 function handleSubmit() {
64 const formData: Record<string, string> = {};
65 refs.current.forEach((input, name) => {
66 formData[name] = input.value;
67 });
68 console.log(formData);
69 }
70
71 return (
72 <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
73 <input ref={setRef("email")} name="email" placeholder="Email" />
74 <input ref={setRef("password")} name="password" type="password" />
75 <button type="submit">Submit</button>
76 </form>
77 );
78}
forwardRef
forward_ref.tsx
TSX
1// forwardRef lets a child component expose its DOM node to a parent
2
3// Without forwardRef — ref prop is not forwarded
4// function MyInput(props) {
5// return <input {...props} />; // ref is NOT forwarded
6// }
7
8// With forwardRef — ref is forwarded to the inner element
9const MyInput = React.forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
10 (props, ref) => {
11 return <input ref={ref} {...props} className="fancy-input" />;
12 }
13);
14
15// Parent usage
16function Parent() {
17 const inputRef = useRef<HTMLInputElement>(null);
18
19 function focus() {
20 inputRef.current?.focus();
21 }
22
23 return (
24 <div>
25 <MyInput ref={inputRef} placeholder="Type here..." />
26 <button onClick={focus}>Focus</button>
27 </div>
28 );
29}
30
31// forwardRef with imperative handle
32interface FormHandle {
33 focus: () => void;
34 reset: () => void;
35 getValues: () => Record<string, string>;
36}
37
38const FancyInput = React.forwardRef<FormHandle, { name: string }>(
39 ({ name }, ref) => {
40 const inputRef = useRef<HTMLInputElement>(null);
41 const [value, setValue] = useState("");
42
43 useImperativeHandle(ref, () => ({
44 focus: () => inputRef.current?.focus(),
45 reset: () => {
46 setValue("");
47 inputRef.current?.value = "";
48 },
49 getValues: () => ({ [name]: value }),
50 }));
51
52 return (
53 <input
54 ref={inputRef}
55 value={value}
56 onChange={(e) => setValue(e.target.value)}
57 placeholder={name}
58 />
59 );
60 }
61);
62
63// Forwarding ref through multiple layers
64const InnerInput = React.forwardRef<HTMLInputElement, InputProps>(
65 (props, ref) => <input ref={ref} {...props} />
66);
67
68const OuterWrapper = React.forwardRef<HTMLInputElement, InputProps>(
69 (props, ref) => (
70 <div className="wrapper">
71 <InnerInput ref={ref} {...props} />
72 </div>
73 )
74);
75
76// In React 19, you can pass ref as a regular prop — no forwardRef needed:
77// function MyInput({ ref, ...props }) {
78// return <input ref={ref} {...props} />;
79// }

info

In React 19, ref is a regular prop — you can pass it without forwardRef. For React 18 and below, use forwardRef to pass refs to child components.
useImperativeHandle
imperative_handle.tsx
TSX
1// useImperativeHandle customizes the ref value exposed to parents
2
3interface VideoPlayerHandle {
4 play: () => void;
5 pause: () => void;
6 seek: (time: number) => void;
7 getDuration: () => number;
8}
9
10const VideoPlayer = React.forwardRef<VideoPlayerHandle, { src: string }>(
11 ({ src }, ref) => {
12 const videoRef = useRef<HTMLVideoElement>(null);
13
14 useImperativeHandle(ref, () => ({
15 play: () => videoRef.current?.play(),
16 pause: () => videoRef.current?.pause(),
17 seek: (time: number) => {
18 if (videoRef.current) videoRef.current.currentTime = time;
19 },
20 getDuration: () => videoRef.current?.duration ?? 0,
21 }));
22
23 return <video ref={videoRef} src={src} className="w-full" />;
24 }
25);
26
27// Parent controls the player through the ref
28function MediaPage() {
29 const playerRef = useRef<VideoPlayerHandle>(null);
30
31 return (
32 <div>
33 <VideoPlayer ref={playerRef} src="/video.mp4" />
34 <div className="controls">
35 <button onClick={() => playerRef.current?.play()}>Play</button>
36 <button onClick={() => playerRef.current?.pause()}>Pause</button>
37 <button onClick={() => playerRef.current?.seek(30)}>
38 Skip to 30s
39 </button>
40 </div>
41 </div>
42 );
43}
44
45// Form component with imperative API
46interface FormRef {
47 submit: () => Promise<boolean>;
48 reset: () => void;
49 isValid: () => boolean;
50}
51
52const Form = React.forwardRef<FormRef, { children: React.ReactNode }>(
53 ({ children }, ref) => {
54 const [errors, setErrors] = useState<Record<string, string>>({});
55
56 useImperativeHandle(ref, () => ({
57 submit: async () => {
58 const isValid = Object.keys(errors).length === 0;
59 if (isValid) {
60 // submit logic
61 return true;
62 }
63 return false;
64 },
65 reset: () => {
66 setErrors({});
67 },
68 isValid: () => Object.keys(errors).length === 0,
69 }));
70
71 return <form>{children}</form>;
72 }
73);
Refs vs State
refs_vs_state.tsx
TSX
1// Key difference: refs don't trigger re-renders, state does
2
3// Use STATE when:
4// - Value changes should update the UI
5// - Value needs to be passed to children
6// - Value is derived from props or other state
7
8function Counter() {
9 const [count, setCount] = useState(0); // UI updates on change
10 return <p>{count}</p>;
11}
12
13// Use REFS when:
14// - Accessing DOM nodes (focus, measurements, animations)
15// - Storing values that persist across renders but don't affect UI
16// - Storing interval/timeout IDs
17// - Storing previous values
18
19function MeasureBox() {
20 const boxRef = useRef<HTMLDivElement>(null);
21 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
22
23 useEffect(() => {
24 if (!boxRef.current) return;
25 const { width, height } = boxRef.current.getBoundingClientRect();
26 setDimensions({ width, height });
27 }, []);
28
29 return (
30 <div ref={boxRef} className="box">
31 <p>{dimensions.width}x{dimensions.height}</p>
32 </div>
33 );
34}
35
36// Ref for mutable state that shouldn't cause re-renders
37function useInterval(callback: () => void, delay: number | null) {
38 const savedCallback = useRef(callback);
39
40 useEffect(() => {
41 savedCallback.current = callback;
42 }, [callback]);
43
44 useEffect(() => {
45 if (delay === null) return;
46 const id = setInterval(() => savedCallback.current(), delay);
47 return () => clearInterval(id);
48 }, [delay]);
49}
50
51// Anti-pattern: reading state in setTimeout/useInterval
52// The closure captures the state at render time, not the current value
53// Use useRef to always have the latest value
54
55// ✅ Correct pattern
56function LatestValueExample() {
57 const [count, setCount] = useState(0);
58 const countRef = useRef(count);
59
60 useEffect(() => {
61 countRef.current = count;
62 }, [count]);
63
64 useEffect(() => {
65 const timer = setTimeout(() => {
66 console.log(`Latest count: ${countRef.current}`);
67 }, 5000);
68 return () => clearTimeout(timer);
69 }, []);
70}
Uncontrolled Components
uncontrolled.tsx
TSX
1// Uncontrolled components store their own state internally
2// React accesses the value via ref when needed
3
4// Controlled (React manages state):
5function ControlledInput() {
6 const [value, setValue] = useState("");
7 return <input value={value} onChange={(e) => setValue(e.target.value)} />;
8}
9
10// Uncontrolled (DOM manages state):
11function UncontrolledInput() {
12 const inputRef = useRef<HTMLInputElement>(null);
13
14 function handleSubmit() {
15 const value = inputRef.current?.value;
16 console.log("Submitted:", value);
17 }
18
19 return (
20 <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
21 <input ref={inputRef} defaultValue="initial" />
22 <button type="submit">Submit</button>
23 </form>
24 );
25}
26
27// Uncontrolled file input — always uncontrolled (no value prop)
28function FileUpload() {
29 const fileRef = useRef<HTMLInputElement>(null);
30
31 function handleUpload() {
32 const files = fileRef.current?.files;
33 if (files && files.length > 0) {
34 console.log("Selected:", files[0].name);
35 }
36 }
37
38 return (
39 <div>
40 <input ref={fileRef} type="file" />
41 <button onClick={handleUpload}>Upload</button>
42 </div>
43 );
44}
45
46// When to use uncontrolled:
47// - Quick forms where you don't need real-time validation
48// - File inputs (always uncontrolled)
49// - Integrating with non-React code
50// - Performance-critical inputs with many rapid changes
51
52// When to use controlled:
53// - Real-time validation
54// - Dynamic form fields
55// - Conditionally disabling inputs
56// - Input formatting (masking)
57// - Submitting form data programmatically

best practice

Prefer controlled components for form inputs in React — they make validation, transformation, and conditional logic straightforward. Use uncontrolled components only for simple forms or when integrating with non-React code.
$Blueprint — Engineering Documentation·Section ID: REACT-REFS·Revision: 1.0