Master useRef for DOM access, imperative operations, and integration with third-party libraries. Learn advanced ref patterns and when to break out of React's declarative paradigm.
While React promotes declarative programming, there are scenarios where you need to access DOM elements directly or store mutable values that persist across renders. useRef is React's solution for these imperative operations, providing a bridge between React's declarative world and the imperative DOM API.
useRef serves two primary purposes: accessing DOM elements and storing mutable values that don't trigger re-renders. Understanding when and how to use refs effectively is crucial for building complex components, integrating with third-party libraries, and implementing performance-critical features.
In this comprehensive lesson, you'll learn advanced ref patterns, master DOM manipulation techniques, and discover how to seamlessly integrate imperative libraries while maintaining React's best practices and performance characteristics.
useRef creates a mutable reference object with a .current property that persists across renders without triggering re-renders when changed.
Basic useRef Usage:
1import { useRef, useEffect } from 'react';
2
3function FocusInput() {
4 const inputRef = useRef(null);
5
6 useEffect(() => {
7 // Focus the input on mount
8 inputRef.current?.focus();
9 }, []);
10
11 const handleReset = () => {
12 // Clear and focus input
13 if (inputRef.current) {
14 inputRef.current.value = '';
15 inputRef.current.focus();
16 }
17 };
18
19 return (
20 <div>
21 <input ref={inputRef} type="text" placeholder="Type here..." />
22 <button onClick={handleReset}>Reset & Focus</button>
23 </div>
24 );
25}Storing Mutable Values:
1function Timer() {
2 const [seconds, setSeconds] = useState(0);
3 const intervalRef = useRef(null);
4 const startTimeRef = useRef(null);
5
6 const start = () => {
7 if (intervalRef.current) return; // Already running
8
9 startTimeRef.current = Date.now() - seconds * 1000;
10 intervalRef.current = setInterval(() => {
11 setSeconds(Math.floor((Date.now() - startTimeRef.current) / 1000));
12 }, 100);
13 };
14
15 const stop = () => {
16 if (intervalRef.current) {
17 clearInterval(intervalRef.current);
18 intervalRef.current = null;
19 }
20 };
21
22 const reset = () => {
23 stop();
24 setSeconds(0);
25 };
26
27 useEffect(() => {
28 return () => stop(); // Cleanup on unmount
29 }, []);
30
31 return (
32 <div>
33 <div>Time: {seconds}s</div>
34 <button onClick={start}>Start</button>
35 <button onClick={stop}>Stop</button>
36 <button onClick={reset}>Reset</button>
37 </div>
38 );
39}Ref vs State Comparison:
1function RefVsState() {
2 const [stateValue, setStateValue] = useState(0);
3 const refValue = useRef(0);
4 const renderCount = useRef(0);
5
6 renderCount.current += 1;
7
8 const updateState = () => {
9 setStateValue(prev => prev + 1); // Triggers re-render
10 };
11
12 const updateRef = () => {
13 refValue.current += 1; // No re-render
14 console.log('Ref value:', refValue.current);
15 };
16
17 return (
18 <div>
19 <div>Renders: {renderCount.current}</div>
20 <div>State: {stateValue}</div>
21 <div>Ref: {refValue.current}</div>
22
23 <button onClick={updateState}>Update State</button>
24 <button onClick={updateRef}>Update Ref</button>
25 </div>
26 );
27}Advanced Ref Patterns:
1. Multiple Element References:
1function MultipleRefs() {
2 const itemRefs = useRef([]);
3
4 // Ensure refs array matches items length
5 const setItemRef = (index) => (element) => {
6 itemRefs.current[index] = element;
7 };
8
9 const focusItem = (index) => {
10 itemRefs.current[index]?.focus();
11 };
12
13 const items = ['First', 'Second', 'Third'];
14
15 return (
16 <div>
17 {items.map((item, index) => (
18 <div key={index}>
19 <input
20 ref={setItemRef(index)}
21 placeholder={`${item} input`}
22 />
23 <button onClick={() => focusItem(index)}>
24 Focus {item}
25 </button>
26 </div>
27 ))}
28 </div>
29 );
30}2. Dynamic Ref Collections:
1function DynamicRefList({ items }) {
2 const itemRefs = useRef(new Map());
3
4 const setItemRef = (id) => (element) => {
5 if (element) {
6 itemRefs.current.set(id, element);
7 } else {
8 itemRefs.current.delete(id);
9 }
10 };
11
12 const scrollToItem = (id) => {
13 const element = itemRefs.current.get(id);
14 element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
15 };
16
17 return (
18 <div>
19 {items.map(item => (
20 <div
21 key={item.id}
22 ref={setItemRef(item.id)}
23 style={{ height: '100px', border: '1px solid #ccc', margin: '10px' }}
24 >
25 {item.name}
26 <button onClick={() => scrollToItem(item.id)}>
27 Scroll to me
28 </button>
29 </div>
30 ))}
31 </div>
32 );
33}3. Ref Forwarding:
1const CustomInput = forwardRef(({ label, ...props }, ref) => {
2 return (
3 <div>
4 <label>{label}</label>
5 <input ref={ref} {...props} />
6 </div>
7 );
8});
9
10function ParentComponent() {
11 const inputRef = useRef(null);
12
13 const focusInput = () => {
14 inputRef.current?.focus();
15 };
16
17 return (
18 <div>
19 <CustomInput
20 ref={inputRef}
21 label="Name:"
22 placeholder="Enter your name"
23 />
24 <button onClick={focusInput}>Focus Input</button>
25 </div>
26 );
27}4. Imperative Handle Pattern:
1const VideoPlayer = forwardRef((props, ref) => {
2 const videoRef = useRef(null);
3
4 useImperativeHandle(ref, () => ({
5 play: () => videoRef.current?.play(),
6 pause: () => videoRef.current?.pause(),
7 seek: (time) => {
8 if (videoRef.current) {
9 videoRef.current.currentTime = time;
10 }
11 },
12 getCurrentTime: () => videoRef.current?.currentTime || 0,
13 getDuration: () => videoRef.current?.duration || 0
14 }));
15
16 return (
17 <video
18 ref={videoRef}
19 controls
20 {...props}
21 />
22 );
23});
24
25function VideoController() {
26 const videoRef = useRef(null);
27
28 const handleJumpTo = (seconds) => {
29 videoRef.current?.seek(seconds);
30 };
31
32 return (
33 <div>
34 <VideoPlayer
35 ref={videoRef}
36 src="/video.mp4"
37 />
38 <button onClick={() => handleJumpTo(30)}>
39 Jump to 30s
40 </button>
41 </div>
42 );
43}These patterns provide the foundation for more complex DOM manipulation and imperative operations in React applications.
Direct DOM manipulation in React requires careful consideration to avoid conflicts with React's virtual DOM and maintain performance.
Measuring and Observing Elements:
1function ElementMeasurer() {
2 const elementRef = useRef(null);
3 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
4 const [isVisible, setIsVisible] = useState(false);
5
6 // Resize Observer for dimension tracking
7 useEffect(() => {
8 if (!elementRef.current) return;
9
10 const resizeObserver = new ResizeObserver((entries) => {
11 const { width, height } = entries[0].contentRect;
12 setDimensions({ width, height });
13 });
14
15 resizeObserver.observe(elementRef.current);
16
17 return () => resizeObserver.disconnect();
18 }, []);
19
20 // Intersection Observer for visibility tracking
21 useEffect(() => {
22 if (!elementRef.current) return;
23
24 const intersectionObserver = new IntersectionObserver(
25 ([entry]) => {
26 setIsVisible(entry.isIntersecting);
27 },
28 { threshold: 0.1 }
29 );
30
31 intersectionObserver.observe(elementRef.current);
32
33 return () => intersectionObserver.disconnect();
34 }, []);
35
36 return (
37 <div
38 ref={elementRef}
39 style={{
40 width: '50%',
41 height: '200px',
42 backgroundColor: isVisible ? 'lightgreen' : 'lightcoral',
43 transition: 'background-color 0.3s'
44 }}
45 >
46 <div>Width: {Math.round(dimensions.width)}px</div>
47 <div>Height: {Math.round(dimensions.height)}px</div>
48 <div>Visible: {isVisible ? 'Yes' : 'No'}</div>
49 </div>
50 );
51}Scroll Management:
1function ScrollManager() {
2 const containerRef = useRef(null);
3 const [scrollInfo, setScrollInfo] = useState({
4 scrollTop: 0,
5 scrollHeight: 0,
6 clientHeight: 0,
7 isAtTop: true,
8 isAtBottom: false
9 });
10
11 // Scroll event handler
12 const handleScroll = useCallback(() => {
13 if (!containerRef.current) return;
14
15 const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
16
17 setScrollInfo({
18 scrollTop,
19 scrollHeight,
20 clientHeight,
21 isAtTop: scrollTop === 0,
22 isAtBottom: scrollTop + clientHeight >= scrollHeight - 1
23 });
24 }, []);
25
26 // Scroll controls
27 const scrollTo = (position) => {
28 containerRef.current?.scrollTo({
29 top: position,
30 behavior: 'smooth'
31 });
32 };
33
34 const scrollToTop = () => scrollTo(0);
35 const scrollToBottom = () => {
36 if (containerRef.current) {
37 scrollTo(containerRef.current.scrollHeight);
38 }
39 };
40
41 const scrollToElement = (elementId) => {
42 const element = containerRef.current?.querySelector(`#${elementId}`);
43 element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
44 };
45
46 return (
47 <div>
48 <div className="controls">
49 <button onClick={scrollToTop} disabled={scrollInfo.isAtTop}>
50 Top
51 </button>
52 <button onClick={scrollToBottom} disabled={scrollInfo.isAtBottom}>
53 Bottom
54 </button>
55 <button onClick={() => scrollToElement('item-50')}>
56 Go to Item 50
57 </button>
58 </div>
59
60 <div
61 ref={containerRef}
62 onScroll={handleScroll}
63 style={{
64 height: '300px',
65 overflow: 'auto',
66 border: '1px solid #ccc'
67 }}
68 >
69 {Array.from({ length: 100 }, (_, i) => (
70 <div
71 key={i}
72 id={`item-${i}`}
73 style={{
74 padding: '20px',
75 borderBottom: '1px solid #eee'
76 }}
77 >
78 Item {i}
79 </div>
80 ))}
81 </div>
82
83 <div className="scroll-info">
84 <div>Scroll: {Math.round(scrollInfo.scrollTop)}px</div>
85 <div>Progress: {Math.round((scrollInfo.scrollTop / (scrollInfo.scrollHeight - scrollInfo.clientHeight)) * 100)}%</div>
86 </div>
87 </div>
88 );
89}Canvas and Graphics Manipulation:
1function CanvasDrawing() {
2 const canvasRef = useRef(null);
3 const isDrawingRef = useRef(false);
4 const lastPositionRef = useRef({ x: 0, y: 0 });
5
6 const startDrawing = useCallback((event) => {
7 const canvas = canvasRef.current;
8 if (!canvas) return;
9
10 const rect = canvas.getBoundingClientRect();
11 const x = event.clientX - rect.left;
12 const y = event.clientY - rect.top;
13
14 isDrawingRef.current = true;
15 lastPositionRef.current = { x, y };
16 }, []);
17
18 const draw = useCallback((event) => {
19 if (!isDrawingRef.current || !canvasRef.current) return;
20
21 const canvas = canvasRef.current;
22 const ctx = canvas.getContext('2d');
23 const rect = canvas.getBoundingClientRect();
24 const x = event.clientX - rect.left;
25 const y = event.clientY - rect.top;
26
27 ctx.beginPath();
28 ctx.moveTo(lastPositionRef.current.x, lastPositionRef.current.y);
29 ctx.lineTo(x, y);
30 ctx.stroke();
31
32 lastPositionRef.current = { x, y };
33 }, []);
34
35 const stopDrawing = useCallback(() => {
36 isDrawingRef.current = false;
37 }, []);
38
39 const clearCanvas = () => {
40 const canvas = canvasRef.current;
41 if (!canvas) return;
42
43 const ctx = canvas.getContext('2d');
44 ctx.clearRect(0, 0, canvas.width, canvas.height);
45 };
46
47 // Initialize canvas
48 useEffect(() => {
49 const canvas = canvasRef.current;
50 if (!canvas) return;
51
52 const ctx = canvas.getContext('2d');
53 ctx.strokeStyle = '#000';
54 ctx.lineWidth = 2;
55 ctx.lineCap = 'round';
56 }, []);
57
58 return (
59 <div>
60 <canvas
61 ref={canvasRef}
62 width={400}
63 height={300}
64 style={{ border: '1px solid #ccc', cursor: 'crosshair' }}
65 onMouseDown={startDrawing}
66 onMouseMove={draw}
67 onMouseUp={stopDrawing}
68 onMouseLeave={stopDrawing}
69 />
70 <div>
71 <button onClick={clearCanvas}>Clear</button>
72 </div>
73 </div>
74 );
75}Form Focus Management:
1function AdvancedForm() {
2 const formRef = useRef(null);
3 const firstErrorRef = useRef(null);
4 const [errors, setErrors] = useState({});
5
6 const validateAndSubmit = (event) => {
7 event.preventDefault();
8
9 const formData = new FormData(formRef.current);
10 const newErrors = {};
11
12 // Validation logic
13 if (!formData.get('email')) {
14 newErrors.email = 'Email is required';
15 }
16 if (!formData.get('password')) {
17 newErrors.password = 'Password is required';
18 }
19
20 setErrors(newErrors);
21
22 if (Object.keys(newErrors).length > 0) {
23 // Focus first error field
24 setTimeout(() => {
25 const firstErrorField = formRef.current?.querySelector('[data-error="true"]');
26 firstErrorField?.focus();
27 }, 0);
28 return;
29 }
30
31 // Submit form
32 console.log('Form submitted!');
33 };
34
35 const focusFirstError = () => {
36 const firstErrorField = formRef.current?.querySelector('[data-error="true"]');
37 firstErrorField?.focus();
38 };
39
40 return (
41 <form ref={formRef} onSubmit={validateAndSubmit}>
42 <div>
43 <label>Email:</label>
44 <input
45 name="email"
46 type="email"
47 data-error={!!errors.email}
48 style={{
49 borderColor: errors.email ? 'red' : 'initial'
50 }}
51 />
52 {errors.email && <div style={{ color: 'red' }}>{errors.email}</div>}
53 </div>
54
55 <div>
56 <label>Password:</label>
57 <input
58 name="password"
59 type="password"
60 data-error={!!errors.password}
61 style={{
62 borderColor: errors.password ? 'red' : 'initial'
63 }}
64 />
65 {errors.password && <div style={{ color: 'red' }}>{errors.password}</div>}
66 </div>
67
68 <button type="submit">Submit</button>
69 <button type="button" onClick={focusFirstError}>
70 Focus First Error
71 </button>
72 </form>
73 );
74}These patterns demonstrate how to effectively manipulate the DOM while respecting React's architecture and maintaining good performance.
A comprehensive demonstration of useRef patterns including DOM manipulation, third-party library integration, imperative APIs, and performance optimization techniques.
1import React, { useRef, useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
2
3// =====================================================================
4// Advanced Text Editor Component with Imperative API
5// =====================================================================
6
7const AdvancedTextEditor = forwardRef((props, ref) => {
8 const editorRef = useRef(null);
9 const [content, setContent] = useState('');
10 const [cursorPosition, setCursorPosition] = useState(0);
11 const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 });
12 const undoStackRef = useRef([]);
13 const redoStackRef = useRef([]);
14
15 // Expose imperative API
16 useImperativeHandle(ref, () => ({
17 focus: () => editorRef.current?.focus(),
18 blur: () => editorRef.current?.blur(),
19 insertText: (text) => insertTextAtCursor(text),
20 selectAll: () => selectAll(),
21 getSelection: () => getSelectedText(),
22 undo: () => undo(),
23 redo: () => redo(),
24 clear: () => {
25 setContent('');
26 saveToHistory('');
27 },
28 getContent: () => content,
29 setContent: (newContent) => {
30 setContent(newContent);
31 saveToHistory(newContent);
32 },
33 getCursorPosition: () => cursorPosition,
34 setCursorPosition: (position) => setCursor(position)
35 }));
36
37 const saveToHistory = useCallback((newContent) => {
38 undoStackRef.current.push(content);
39 if (undoStackRef.current.length > 50) {
40 undoStackRef.current.shift();
41 }
42 redoStackRef.current = [];
43 }, [content]);
44
45 const insertTextAtCursor = useCallback((text) => {
46 const textarea = editorRef.current;
47 if (!textarea) return;
48
49 const start = textarea.selectionStart;
50 const end = textarea.selectionEnd;
51 const beforeCursor = content.substring(0, start);
52 const afterCursor = content.substring(end);
53 const newContent = beforeCursor + text + afterCursor;
54
55 setContent(newContent);
56 saveToHistory(newContent);
57
58 // Set cursor position after inserted text
59 setTimeout(() => {
60 textarea.setSelectionRange(start + text.length, start + text.length);
61 }, 0);
62 }, [content, saveToHistory]);
63
64 const selectAll = useCallback(() => {
65 const textarea = editorRef.current;
66 if (!textarea) return;
67
68 textarea.select();
69 setSelectionRange({ start: 0, end: content.length });
70 }, [content]);
71
72 const getSelectedText = useCallback(() => {
73 const textarea = editorRef.current;
74 if (!textarea) return '';
75
76 return content.substring(textarea.selectionStart, textarea.selectionEnd);
77 }, [content]);
78
79 const setCursor = useCallback((position) => {
80 const textarea = editorRef.current;
81 if (!textarea) return;
82
83 textarea.setSelectionRange(position, position);
84 setCursorPosition(position);
85 }, []);
86
87 const undo = useCallback(() => {
88 if (undoStackRef.current.length === 0) return;
89
90 const previousContent = undoStackRef.current.pop();
91 redoStackRef.current.push(content);
92 setContent(previousContent);
93 }, [content]);
94
95 const redo = useCallback(() => {
96 if (redoStackRef.current.length === 0) return;
97
98 const nextContent = redoStackRef.current.pop();
99 undoStackRef.current.push(content);
100 setContent(nextContent);
101 }, [content]);
102
103 const handleSelectionChange = useCallback(() => {
104 const textarea = editorRef.current;
105 if (!textarea) return;
106
107 setCursorPosition(textarea.selectionStart);
108 setSelectionRange({
109 start: textarea.selectionStart,
110 end: textarea.selectionEnd
111 });
112 }, []);
113
114 const handleKeyDown = useCallback((event) => {
115 // Handle keyboard shortcuts
116 if (event.ctrlKey || event.metaKey) {
117 switch (event.key) {
118 case 'z':
119 event.preventDefault();
120 if (event.shiftKey) {
121 redo();
122 } else {
123 undo();
124 }
125 break;
126 case 'a':
127 event.preventDefault();
128 selectAll();
129 break;
130 }
131 }
132 }, [undo, redo, selectAll]);
133
134 const handleChange = useCallback((event) => {
135 const newContent = event.target.value;
136 setContent(newContent);
137 }, []);
138
139 const handleBlur = useCallback(() => {
140 saveToHistory(content);
141 }, [content, saveToHistory]);
142
143 return (
144 <div className="advanced-editor">
145 <div className="editor-toolbar">
146 <span>Cursor: {cursorPosition}</span>
147 <span>Selection: {selectionRange.start !== selectionRange.end ? `${selectionRange.start}-${selectionRange.end}` : 'None'}</span>
148 <span>Length: {content.length}</span>
149 </div>
150
151 <textarea
152 ref={editorRef}
153 value={content}
154 onChange={handleChange}
155 onKeyDown={handleKeyDown}
156 onSelect={handleSelectionChange}
157 onBlur={handleBlur}
158 style={{
159 width: '100%',
160 height: '200px',
161 fontFamily: 'monospace',
162 padding: '10px',
163 border: '1px solid #ccc',
164 resize: 'vertical'
165 }}
166 placeholder="Start typing..."
167 />
168 </div>
169 );
170});
171
172// =====================================================================
173// Drag and Drop File Upload Component
174// =====================================================================
175
176function DragDropUpload({ onFilesUploaded }) {
177 const dropZoneRef = useRef(null);
178 const fileInputRef = useRef(null);
179 const [isDragging, setIsDragging] = useState(false);
180 const [dragCounter, setDragCounter] = useState(0);
181
182 const handleDragEnter = useCallback((event) => {
183 event.preventDefault();
184 setDragCounter(prev => prev + 1);
185 if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
186 setIsDragging(true);
187 }
188 }, []);
189
190 const handleDragLeave = useCallback((event) => {
191 event.preventDefault();
192 setDragCounter(prev => {
193 const newCounter = prev - 1;
194 if (newCounter === 0) {
195 setIsDragging(false);
196 }
197 return newCounter;
198 });
199 }, []);
200
201 const handleDragOver = useCallback((event) => {
202 event.preventDefault();
203 }, []);
204
205 const handleDrop = useCallback((event) => {
206 event.preventDefault();
207 setIsDragging(false);
208 setDragCounter(0);
209
210 const files = Array.from(event.dataTransfer.files);
211 if (files.length > 0) {
212 onFilesUploaded?.(files);
213 }
214 }, [onFilesUploaded]);
215
216 const handleFileInputChange = useCallback((event) => {
217 const files = Array.from(event.target.files || []);
218 if (files.length > 0) {
219 onFilesUploaded?.(files);
220 }
221 // Reset input value to allow re-uploading same file
222 event.target.value = '';
223 }, [onFilesUploaded]);
224
225 const openFileDialog = useCallback(() => {
226 fileInputRef.current?.click();
227 }, []);
228
229 // Prevent default drag behavior on document
230 useEffect(() => {
231 const preventDefaults = (event) => {
232 event.preventDefault();
233 event.stopPropagation();
234 };
235
236 document.addEventListener('dragenter', preventDefaults);
237 document.addEventListener('dragover', preventDefaults);
238 document.addEventListener('dragleave', preventDefaults);
239 document.addEventListener('drop', preventDefaults);
240
241 return () => {
242 document.removeEventListener('dragenter', preventDefaults);
243 document.removeEventListener('dragover', preventDefaults);
244 document.removeEventListener('dragleave', preventDefaults);
245 document.removeEventListener('drop', preventDefaults);
246 };
247 }, []);
248
249 return (
250 <div
251 ref={dropZoneRef}
252 onDragEnter={handleDragEnter}
253 onDragLeave={handleDragLeave}
254 onDragOver={handleDragOver}
255 onDrop={handleDrop}
256 onClick={openFileDialog}
257 style={{
258 border: `2px dashed ${isDragging ? '#007bff' : '#ccc'}`,
259 borderRadius: '8px',
260 padding: '40px',
261 textAlign: 'center',
262 backgroundColor: isDragging ? '#f8f9fa' : 'transparent',
263 cursor: 'pointer',
264 transition: 'all 0.3s ease'
265 }}
266 >
267 <input
268 ref={fileInputRef}
269 type="file"
270 multiple
271 onChange={handleFileInputChange}
272 style={{ display: 'none' }}
273 />
274
275 <div>
276 <div style={{ fontSize: '48px', marginBottom: '16px' }}>
277 {isDragging ? '📁' : '📤'}
278 </div>
279 <div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }}>
280 {isDragging ? 'Drop files here' : 'Drag & drop files here'}
281 </div>
282 <div style={{ color: '#666' }}>
283 or click to select files
284 </div>
285 </div>
286 </div>
287 );
288}
289
290// =====================================================================
291// Virtual Scrolling List Component
292// =====================================================================
293
294function VirtualScrollList({ items, itemHeight = 50, containerHeight = 300 }) {
295 const containerRef = useRef(null);
296 const [scrollTop, setScrollTop] = useState(0);
297 const [containerSize, setContainerSize] = useState({ width: 0, height: containerHeight });
298
299 // Calculate visible range
300 const visibleRange = React.useMemo(() => {
301 const startIndex = Math.floor(scrollTop / itemHeight);
302 const endIndex = Math.min(
303 startIndex + Math.ceil(containerSize.height / itemHeight) + 1,
304 items.length - 1
305 );
306
307 return { startIndex, endIndex };
308 }, [scrollTop, itemHeight, containerSize.height, items.length]);
309
310 // Handle scroll
311 const handleScroll = useCallback((event) => {
312 setScrollTop(event.target.scrollTop);
313 }, []);
314
315 // Handle container resize
316 useEffect(() => {
317 if (!containerRef.current) return;
318
319 const resizeObserver = new ResizeObserver((entries) => {
320 const { width, height } = entries[0].contentRect;
321 setContainerSize({ width, height });
322 });
323
324 resizeObserver.observe(containerRef.current);
325
326 return () => resizeObserver.disconnect();
327 }, []);
328
329 const totalHeight = items.length * itemHeight;
330 const offsetY = visibleRange.startIndex * itemHeight;
331
332 return (
333 <div
334 ref={containerRef}
335 onScroll={handleScroll}
336 style={{
337 height: containerHeight,
338 overflow: 'auto',
339 border: '1px solid #ccc'
340 }}
341 >
342 <div style={{ height: totalHeight, position: 'relative' }}>
343 <div
344 style={{
345 transform: `translateY(${offsetY}px)`,
346 position: 'absolute',
347 top: 0,
348 left: 0,
349 right: 0
350 }}
351 >
352 {items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, index) => {
353 const actualIndex = visibleRange.startIndex + index;
354 return (
355 <div
356 key={actualIndex}
357 style={{
358 height: itemHeight,
359 display: 'flex',
360 alignItems: 'center',
361 padding: '0 16px',
362 borderBottom: '1px solid #eee'
363 }}
364 >
365 <span>#{actualIndex}</span>
366 <span style={{ marginLeft: '16px' }}>{item}</span>
367 </div>
368 );
369 })}
370 </div>
371 </div>
372 </div>
373 );
374}
375
376// =====================================================================
377// Interactive Canvas Drawing Component
378// =====================================================================
379
380function InteractiveCanvas() {
381 const canvasRef = useRef(null);
382 const isDrawingRef = useRef(false);
383 const lastPointRef = useRef({ x: 0, y: 0 });
384 const [tool, setTool] = useState('pen');
385 const [color, setColor] = useState('#000000');
386 const [lineWidth, setLineWidth] = useState(2);
387 const historyRef = useRef([]);
388 const historyStepRef = useRef(0);
389
390 const saveState = useCallback(() => {
391 const canvas = canvasRef.current;
392 if (!canvas) return;
393
394 const imageData = canvas.toDataURL();
395 historyRef.current = historyRef.current.slice(0, historyStepRef.current + 1);
396 historyRef.current.push(imageData);
397 historyStepRef.current = historyRef.current.length - 1;
398 }, []);
399
400 const restoreState = useCallback((imageData) => {
401 const canvas = canvasRef.current;
402 if (!canvas) return;
403
404 const ctx = canvas.getContext('2d');
405 const img = new Image();
406 img.onload = () => {
407 ctx.clearRect(0, 0, canvas.width, canvas.height);
408 ctx.drawImage(img, 0, 0);
409 };
410 img.src = imageData;
411 }, []);
412
413 const startDrawing = useCallback((event) => {
414 const canvas = canvasRef.current;
415 if (!canvas) return;
416
417 const rect = canvas.getBoundingClientRect();
418 const x = event.clientX - rect.left;
419 const y = event.clientY - rect.top;
420
421 isDrawingRef.current = true;
422 lastPointRef.current = { x, y };
423
424 const ctx = canvas.getContext('2d');
425 ctx.beginPath();
426 ctx.moveTo(x, y);
427 }, []);
428
429 const draw = useCallback((event) => {
430 if (!isDrawingRef.current || !canvasRef.current) return;
431
432 const canvas = canvasRef.current;
433 const ctx = canvas.getContext('2d');
434 const rect = canvas.getBoundingClientRect();
435 const x = event.clientX - rect.left;
436 const y = event.clientY - rect.top;
437
438 ctx.lineWidth = lineWidth;
439 ctx.lineCap = 'round';
440 ctx.lineJoin = 'round';
441
442 if (tool === 'pen') {
443 ctx.globalCompositeOperation = 'source-over';
444 ctx.strokeStyle = color;
445 } else if (tool === 'eraser') {
446 ctx.globalCompositeOperation = 'destination-out';
447 }
448
449 ctx.lineTo(x, y);
450 ctx.stroke();
451 ctx.beginPath();
452 ctx.moveTo(x, y);
453
454 lastPointRef.current = { x, y };
455 }, [tool, color, lineWidth]);
456
457 const stopDrawing = useCallback(() => {
458 if (isDrawingRef.current) {
459 isDrawingRef.current = false;
460 saveState();
461 }
462 }, [saveState]);
463
464 const clearCanvas = useCallback(() => {
465 const canvas = canvasRef.current;
466 if (!canvas) return;
467
468 const ctx = canvas.getContext('2d');
469 ctx.clearRect(0, 0, canvas.width, canvas.height);
470 saveState();
471 }, [saveState]);
472
473 const undo = useCallback(() => {
474 if (historyStepRef.current > 0) {
475 historyStepRef.current--;
476 restoreState(historyRef.current[historyStepRef.current]);
477 }
478 }, [restoreState]);
479
480 const redo = useCallback(() => {
481 if (historyStepRef.current < historyRef.current.length - 1) {
482 historyStepRef.current++;
483 restoreState(historyRef.current[historyStepRef.current]);
484 }
485 }, [restoreState]);
486
487 // Initialize canvas
488 useEffect(() => {
489 const canvas = canvasRef.current;
490 if (!canvas) return;
491
492 const ctx = canvas.getContext('2d');
493 ctx.fillStyle = 'white';
494 ctx.fillRect(0, 0, canvas.width, canvas.height);
495 saveState();
496 }, [saveState]);
497
498 return (
499 <div className="canvas-container">
500 <div className="canvas-toolbar" style={{ marginBottom: '10px', display: 'flex', gap: '10px', alignItems: 'center' }}>
501 <button
502 onClick={() => setTool('pen')}
503 style={{ backgroundColor: tool === 'pen' ? '#007bff' : 'transparent' }}
504 >
505 ✏️ Pen
506 </button>
507 <button
508 onClick={() => setTool('eraser')}
509 style={{ backgroundColor: tool === 'eraser' ? '#007bff' : 'transparent' }}
510 >
511 🧹 Eraser
512 </button>
513
514 <input
515 type="color"
516 value={color}
517 onChange={(e) => setColor(e.target.value)}
518 disabled={tool === 'eraser'}
519 />
520
521 <input
522 type="range"
523 min="1"
524 max="20"
525 value={lineWidth}
526 onChange={(e) => setLineWidth(parseInt(e.target.value))}
527 />
528 <span>{lineWidth}px</span>
529
530 <button onClick={undo}>↶ Undo</button>
531 <button onClick={redo}>↷ Redo</button>
532 <button onClick={clearCanvas}>🗑️ Clear</button>
533 </div>
534
535 <canvas
536 ref={canvasRef}
537 width={500}
538 height={300}
539 style={{
540 border: '1px solid #ccc',
541 cursor: tool === 'pen' ? 'crosshair' : 'grab'
542 }}
543 onMouseDown={startDrawing}
544 onMouseMove={draw}
545 onMouseUp={stopDrawing}
546 onMouseLeave={stopDrawing}
547 />
548 </div>
549 );
550}
551
552// =====================================================================
553// Main Demo Application
554// =====================================================================
555
556export default function AdvancedRefDemo() {
557 const editorRef = useRef(null);
558 const [uploadedFiles, setUploadedFiles] = useState([]);
559 const [editorContent, setEditorContent] = useState('Welcome to the advanced useRef demonstration!\n\nThis editor supports:\n- Undo/Redo (Ctrl+Z/Ctrl+Shift+Z)\n- Select All (Ctrl+A)\n- Programmatic content manipulation\n\nTry the buttons below to test the imperative API!');
560
561 // Virtual list data
562 const virtualListItems = React.useMemo(() =>
563 Array.from({ length: 10000 }, (_, i) => `Virtual Item ${i + 1}`)
564 , []);
565
566 const handleFilesUploaded = useCallback((files) => {
567 setUploadedFiles(prev => [...prev, ...files.map(file => ({
568 name: file.name,
569 size: file.size,
570 type: file.type,
571 id: Date.now() + Math.random()
572 }))]);
573 }, []);
574
575 const clearUploadedFiles = useCallback(() => {
576 setUploadedFiles([]);
577 }, []);
578
579 // Editor control functions
580 const insertDateTime = () => {
581 editorRef.current?.insertText(`\n\nCurrent time: ${new Date().toLocaleString()}\n`);
582 };
583
584 const insertLoremIpsum = () => {
585 const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ';
586 editorRef.current?.insertText(lorem);
587 };
588
589 const selectAllText = () => {
590 editorRef.current?.selectAll();
591 };
592
593 const getSelectedText = () => {
594 const selected = editorRef.current?.getSelection();
595 if (selected) {
596 alert(`Selected text: "${selected}"`);
597 } else {
598 alert('No text selected');
599 }
600 };
601
602 const undoEdit = () => {
603 editorRef.current?.undo();
604 };
605
606 const redoEdit = () => {
607 editorRef.current?.redo();
608 };
609
610 return (
611 <div className="min-h-screen bg-gray-100 p-6">
612 <div className="max-w-6xl mx-auto">
613 <div className="mb-8">
614 <h1 className="text-3xl font-bold text-gray-800 mb-2">
615 🎯 Advanced useRef & DOM Manipulation
616 </h1>
617 <p className="text-gray-600">
618 Comprehensive demonstration of useRef patterns, DOM manipulation, and imperative APIs
619 </p>
620 </div>
621
622 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
623 {/* Advanced Text Editor */}
624 <div className="bg-white rounded-lg shadow-lg p-6">
625 <h2 className="text-xl font-bold mb-4">📝 Advanced Text Editor</h2>
626 <p className="text-sm text-gray-600 mb-4">
627 Text editor with imperative API, undo/redo, and keyboard shortcuts
628 </p>
629
630 <AdvancedTextEditor
631 ref={editorRef}
632 initialContent={editorContent}
633 />
634
635 <div className="mt-4 flex flex-wrap gap-2">
636 <button
637 onClick={insertDateTime}
638 className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
639 >
640 Insert DateTime
641 </button>
642 <button
643 onClick={insertLoremIpsum}
644 className="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600"
645 >
646 Insert Lorem
647 </button>
648 <button
649 onClick={selectAllText}
650 className="px-3 py-1 text-sm bg-purple-500 text-white rounded hover:bg-purple-600"
651 >
652 Select All
653 </button>
654 <button
655 onClick={getSelectedText}
656 className="px-3 py-1 text-sm bg-orange-500 text-white rounded hover:bg-orange-600"
657 >
658 Get Selection
659 </button>
660 <button
661 onClick={undoEdit}
662 className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600"
663 >
664 Undo
665 </button>
666 <button
667 onClick={redoEdit}
668 className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600"
669 >
670 Redo
671 </button>
672 </div>
673 </div>
674
675 {/* Drag & Drop Upload */}
676 <div className="bg-white rounded-lg shadow-lg p-6">
677 <h2 className="text-xl font-bold mb-4">📤 Drag & Drop Upload</h2>
678 <p className="text-sm text-gray-600 mb-4">
679 Advanced drag and drop with file handling and visual feedback
680 </p>
681
682 <DragDropUpload onFilesUploaded={handleFilesUploaded} />
683
684 {uploadedFiles.length > 0 && (
685 <div className="mt-4">
686 <div className="flex items-center justify-between mb-2">
687 <h3 className="font-semibold">Uploaded Files ({uploadedFiles.length})</h3>
688 <button
689 onClick={clearUploadedFiles}
690 className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
691 >
692 Clear All
693 </button>
694 </div>
695 <div className="space-y-1 max-h-32 overflow-y-auto">
696 {uploadedFiles.map(file => (
697 <div key={file.id} className="text-sm p-2 bg-gray-50 rounded">
698 <div className="font-medium">{file.name}</div>
699 <div className="text-gray-500">
700 {(file.size / 1024).toFixed(1)} KB • {file.type || 'Unknown type'}
701 </div>
702 </div>
703 ))}
704 </div>
705 </div>
706 )}
707 </div>
708
709 {/* Virtual Scrolling List */}
710 <div className="bg-white rounded-lg shadow-lg p-6">
711 <h2 className="text-xl font-bold mb-4">📋 Virtual Scrolling List</h2>
712 <p className="text-sm text-gray-600 mb-4">
713 High-performance virtual scrolling with 10,000 items
714 </p>
715
716 <VirtualScrollList
717 items={virtualListItems}
718 itemHeight={50}
719 containerHeight={300}
720 />
721 </div>
722
723 {/* Interactive Canvas */}
724 <div className="bg-white rounded-lg shadow-lg p-6">
725 <h2 className="text-xl font-bold mb-4">🎨 Interactive Canvas</h2>
726 <p className="text-sm text-gray-600 mb-4">
727 Canvas drawing with tools, undo/redo, and direct DOM manipulation
728 </p>
729
730 <InteractiveCanvas />
731 </div>
732 </div>
733
734 <div className="mt-8 p-6 bg-blue-50 border border-blue-200 rounded-lg">
735 <h3 className="font-semibold text-blue-800 mb-3">🎯 useRef Patterns Demonstrated</h3>
736 <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-700">
737 <div>
738 <strong>✅ Imperative APIs</strong>
739 <p>forwardRef and useImperativeHandle for controlled component APIs</p>
740 </div>
741 <div>
742 <strong>✅ DOM Manipulation</strong>
743 <p>Direct canvas drawing, drag/drop, scroll management</p>
744 </div>
745 <div>
746 <strong>✅ Performance Optimization</strong>
747 <p>Virtual scrolling, event handling, state preservation</p>
748 </div>
749 <div>
750 <strong>✅ Third-party Integration</strong>
751 <p>Observer APIs, file handling, canvas operations</p>
752 </div>
753 </div>
754 </div>
755 </div>
756 </div>
757 );
758}This comprehensive useRef demonstration showcases advanced patterns for DOM manipulation and imperative operations:
🏗️ Advanced useRef Patterns:
1. Imperative Component APIs:
2. Complex DOM Interactions:
3. Performance-Critical Operations:
4. Canvas and Graphics:
5. Advanced DOM Management:
6. Real-World Integration Scenarios:
7. Performance Optimization Techniques:
8. Production-Ready Features:
This implementation demonstrates when and how to effectively use imperative patterns in React while maintaining performance, accessibility, and code maintainability.
Outstanding work mastering useRef and DOM manipulation! You now have the skills to handle complex DOM interactions, build imperative APIs, and integrate with third-party libraries while maintaining React's performance and best practices. In our next lesson, we'll explore useContext Advanced Patterns, where you'll learn to build scalable context architectures, implement compound components, and design performant global state management systems.
Your progress is saved in this browser, on this device only.