Refs and DOM Manipulation
1. useRef Hook for DOM Element Access
| Concept | Description | Use Case |
|---|---|---|
| useRef Hook | Creates mutable ref object with .current property that persists across renders | DOM access, storing mutable values, keeping previous values |
| DOM Ref | Reference to actual DOM element when assigned via ref attribute | Focus management, scroll control, measuring elements |
| Ref Stability | Ref object remains the same across renders, only .current changes | Avoid re-renders when storing mutable values |
| No Re-render | Changing ref.current doesn't trigger component re-render | Storing timers, intervals, subscriptions |
Example: Basic DOM Element Access
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Access DOM element after mount
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleClear = () => {
if (inputRef.current) {
inputRef.current.value = '';
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClear}>Clear</button>
</div>
);
}
Example: Multiple Refs with Array
function MultipleInputs() {
const inputRefs = useRef([]);
const focusInput = (index) => {
inputRefs.current[index]?.focus();
};
return (
<div>
{[0, 1, 2].map((index) => (
<input
key={index}
ref={(el) => inputRefs.current[index] = el}
onKeyPress={(e) => {
if (e.key === 'Enter' && index < 2) {
focusInput(index + 1);
}
}}
/>
))}
</div>
);
}
Example: Storing Mutable Values
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const renderCountRef = useRef(0);
useEffect(() => {
// Track render count without causing re-renders
renderCountRef.current += 1;
});
const startTimer = () => {
if (!intervalRef.current) {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
}
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => stopTimer(); // Cleanup on unmount
}, []);
return (
<div>
<p>Count: {count}</p>
<p>Renders: {renderCountRef.current}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
| Ref Pattern | Syntax | When to Use |
|---|---|---|
| Initial Value | useRef(initialValue) |
Set .current to initial value (useful for non-DOM refs) |
| Null DOM Ref | useRef(null) |
Standard pattern for DOM element refs |
| Callback Ref | ref={(el) => ...} |
Need to run code when ref attaches/detaches |
| Conditional Ref | ref={condition ? myRef : null} |
Conditionally attach ref |
2. forwardRef and Ref Forwarding Patterns
| Concept | Description | Use Case |
|---|---|---|
| forwardRef | HOC that allows component to receive and pass ref to child element | Custom components that need to expose DOM element |
| Ref Forwarding | Technique to automatically pass ref through component to child | Reusable UI components (Input, Button wrappers) |
| Display Name | Set component.displayName for better debugging with forwardRef | DevTools shows meaningful names |
| Generic Refs | TypeScript generic type for typed ref forwarding | Type-safe ref forwarding in TS |
Example: Basic Ref Forwarding
import { forwardRef } from 'react';
// Without forwardRef - Won't work
function BrokenInput(props) {
return <input {...props} />; // ref prop not forwarded
}
// With forwardRef - Works correctly
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
CustomInput.displayName = 'CustomInput';
// Usage
function Form() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<CustomInput ref={inputRef} placeholder="Enter text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Example: Styled Component with Ref Forwarding
const FancyButton = forwardRef(({ children, variant = 'primary', ...props }, ref) => {
const className = `btn btn-${variant}`;
return (
<button ref={ref} className={className} {...props}>
{children}
</button>
);
});
FancyButton.displayName = 'FancyButton';
// Usage with ref
function App() {
const btnRef = useRef(null);
const measureButton = () => {
if (btnRef.current) {
const { width, height } = btnRef.current.getBoundingClientRect();
console.log(`Button size: ${width}x${height}`);
}
};
return (
<FancyButton ref={btnRef} variant="success" onClick={measureButton}>
Click Me
</FancyButton>
);
}
Example: Combining Own Ref with Forwarded Ref
const InputWithValidation = forwardRef(({ onValidate, ...props }, forwardedRef) => {
const internalRef = useRef(null);
// Combine both refs
useEffect(() => {
if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(internalRef.current);
} else {
forwardedRef.current = internalRef.current;
}
}
}, [forwardedRef]);
const handleBlur = () => {
if (internalRef.current && onValidate) {
onValidate(internalRef.current.value);
}
};
return (
<input
ref={internalRef}
onBlur={handleBlur}
{...props}
/>
);
});
InputWithValidation.displayName = 'InputWithValidation';
Example: TypeScript Ref Forwarding
import { forwardRef, Ref } from 'react';
interface InputProps {
label: string;
error?: string;
}
const FormInput = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="form-group">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
FormInput.displayName = 'FormInput';
// Usage with type safety
function TypedForm() {
const inputRef = useRef<HTMLInputElement>(null);
return <FormInput ref={inputRef} label="Email" />;
}
Ref Forwarding Best Practices:
- Always set displayName for better DevTools experience
- Use forwardRef for reusable UI components
- Document which DOM element the ref points to
- Consider using useImperativeHandle for custom APIs
- Handle both function and object refs in custom logic
3. useImperativeHandle for Custom Ref APIs
| Concept | Description | Use Case |
|---|---|---|
| useImperativeHandle | Customizes ref instance value exposed to parent components | Expose limited, controlled API instead of full DOM element |
| Ref API | Custom object with methods/properties exposed via ref | Hide implementation, provide semantic methods |
| Encapsulation | Hide internal DOM structure, expose only intended interface | Complex components with internal state/refs |
| Dependencies | Third parameter array, similar to useEffect | Recreate handle when dependencies change |
Example: Custom Input with Imperative Methods
import { forwardRef, useRef, useImperativeHandle } from 'react';
const AdvancedInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
// Expose custom API instead of raw DOM element
useImperativeHandle(ref, () => ({
// Custom methods
focus: () => {
inputRef.current?.focus();
},
clear: () => {
setValue('');
inputRef.current?.focus();
},
getValue: () => value,
setValue: (newValue) => setValue(newValue),
// Expose select DOM methods
scrollIntoView: () => {
inputRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}), [value]); // Recreate when value changes
return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
);
});
// Usage
function Form() {
const inputRef = useRef(null);
const handleSubmit = () => {
const value = inputRef.current?.getValue();
console.log('Value:', value);
inputRef.current?.clear();
};
return (
<div>
<AdvancedInput ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
Example: Modal Component with Imperative API
const Modal = forwardRef(({ children }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState(null);
const modalRef = useRef(null);
useImperativeHandle(ref, () => ({
open: (newContent) => {
setContent(newContent);
setIsOpen(true);
},
close: () => {
setIsOpen(false);
},
toggle: () => {
setIsOpen((prev) => !prev);
},
isOpen: () => isOpen,
updateContent: (newContent) => {
setContent(newContent);
}
}), [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div
ref={modalRef}
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
{content || children}
</div>
</div>
);
});
// Usage
function App() {
const modalRef = useRef(null);
const showSuccess = () => {
modalRef.current?.open(<div>Success!</div>);
setTimeout(() => modalRef.current?.close(), 2000);
};
return (
<div>
<button onClick={showSuccess}>Show Modal</button>
<Modal ref={modalRef} />
</div>
);
}
Example: Video Player with Imperative Controls
const VideoPlayer = forwardRef(({ src }, ref) => {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
stop: () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
}
},
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
setVolume: (volume) => {
if (videoRef.current) {
videoRef.current.volume = Math.max(0, Math.min(1, volume));
}
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0,
getDuration: () => videoRef.current?.duration ?? 0,
isPlaying: () => !videoRef.current?.paused
}));
return <video ref={videoRef} src={src} />;
});
// Usage
function VideoControls() {
const playerRef = useRef(null);
const handleSkip = (seconds) => {
const current = playerRef.current?.getCurrentTime() ?? 0;
playerRef.current?.seek(current + seconds);
};
return (
<div>
<VideoPlayer ref={playerRef} src="video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => handleSkip(-10)}>-10s</button>
<button onClick={() => handleSkip(10)}>+10s</button>
</div>
);
}
| Pattern | Description | Example |
|---|---|---|
| Action Methods | Methods that perform actions (focus, clear, submit) | { focus: () => {...}, clear: () => {...} } |
| Query Methods | Methods that return values (getValue, isValid) | { getValue: () => value, isValid: () => valid } |
| State Methods | Methods that change internal state (open, close) | { open: () => {...}, close: () => {...} } |
| Controlled API | Expose subset of DOM methods with validation | { play: () => video.play(), pause: () => video.pause() } |
useImperativeHandle Caveats:
- Use sparingly - prefer controlled components and props
- Don't expose full DOM element unless necessary
- Document the imperative API thoroughly
- Consider dependencies array to avoid stale closures
- Overuse breaks React's declarative paradigm
4. Focus Management and Accessibility
| Technique | Description | Accessibility Impact |
|---|---|---|
| Auto Focus | Automatically focus element on mount or condition | Improves keyboard navigation, required for modals |
| Focus Trap | Keep focus within container (modal, dropdown) | Prevents focus from escaping modal dialogs |
| Focus Return | Return focus to trigger element after modal closes | Maintains user's place in navigation flow |
| Skip Links | Allow keyboard users to skip navigation | WCAG 2.1 Level A requirement |
| Focus Indicators | Visible outline/highlight for focused elements | Required for keyboard-only users |
| Tab Order | Control tabIndex for logical navigation | Ensures logical focus flow |
Example: Auto Focus on Mount
function LoginForm() {
const usernameRef = useRef(null);
useEffect(() => {
// Focus first input on mount
usernameRef.current?.focus();
}, []);
return (
<form>
<input ref={usernameRef} name="username" placeholder="Username" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
Example: Focus Trap in Modal
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save current focus
previousFocusRef.current = document.activeElement;
// Focus modal
modalRef.current?.focus();
// Setup focus trap
const handleKeyDown = (e) => {
if (e.key === 'Tab') {
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusableElements?.length) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift+Tab
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
// Close on Escape
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
} else {
// Return focus when modal closes
previousFocusRef.current?.focus();
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div
ref={modalRef}
className="modal"
role="dialog"
aria-modal="true"
tabIndex={-1}
>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
Example: Skip Navigation Link
function PageLayout({ children }) {
const mainContentRef = useRef(null);
const skipToMain = (e) => {
e.preventDefault();
mainContentRef.current?.focus();
mainContentRef.current?.scrollIntoView();
};
return (
<>
<a
href="#main"
className="skip-link"
onClick={skipToMain}
style={{
position: 'absolute',
left: '-9999px',
zIndex: 999
}}
onFocus={(e) => {
e.target.style.left = '0';
}}
onBlur={(e) => {
e.target.style.left = '-9999px';
}}
>
Skip to main content
</a>
<nav>
{/* Navigation items */}
</nav>
<main
ref={mainContentRef}
id="main"
tabIndex={-1}
style={{ outline: 'none' }}
>
{children}
</main>
</>
);
}
Example: Focus Management in Dynamic Lists
function TodoList() {
const [todos, setTodos] = useState([]);
const itemRefs = useRef(new Map());
const deleteTodo = (id, index) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
// Focus next item or previous if last
setTimeout(() => {
const nextIndex = Math.min(index, todos.length - 2);
const nextId = todos[nextIndex]?.id;
if (nextId) {
itemRefs.current.get(nextId)?.focus();
}
}, 0);
};
return (
<ul>
{todos.map((todo, index) => (
<li key={todo.id}>
<span>{todo.text}</span>
<button
ref={(el) => {
if (el) {
itemRefs.current.set(todo.id, el);
} else {
itemRefs.current.delete(todo.id);
}
}}
onClick={() => deleteTodo(todo.id, index)}
>
Delete
</button>
</li>
))}
</ul>
);
}
| ARIA Attribute | Purpose | Usage with Refs |
|---|---|---|
| aria-activedescendant | Identifies focused element in composite widget | Update with ref when selection changes |
| aria-describedby | References element describing current element | Link input to error message via ref IDs |
| aria-labelledby | References element(s) labeling current element | Associate labels with inputs using ref IDs |
| aria-live | Announces dynamic content changes | Focus live region ref for announcements |
Focus Management Best Practices:
- Always manage focus when showing/hiding modals
- Return focus to trigger element after dialog closes
- Implement focus trap for modal dialogs
- Provide visible focus indicators (never use outline: none without replacement)
- Test with keyboard-only navigation
- Use semantic HTML elements for better focus behavior
- Consider screen reader announcements with aria-live
- Avoid unnecessary tabIndex values (rely on natural tab order)
5. DOM Measurements and Element Properties
| Measurement | Property/Method | Use Case |
|---|---|---|
| Element Size | getBoundingClientRect() | Get size and position relative to viewport |
| Scroll Position | scrollTop, scrollLeft | Track/control scroll position |
| Offset Dimensions | offsetWidth, offsetHeight | Element dimensions including padding/border |
| Client Dimensions | clientWidth, clientHeight | Element dimensions excluding scrollbars |
| Scroll Dimensions | scrollWidth, scrollHeight | Total scrollable content size |
| Computed Styles | getComputedStyle() | Get actual CSS values applied |
Example: Measuring Element Dimensions
function ElementMeasurements() {
const elementRef = useRef(null);
const [dimensions, setDimensions] = useState({});
useEffect(() => {
const measureElement = () => {
if (elementRef.current) {
const rect = elementRef.current.getBoundingClientRect();
const computedStyle = window.getComputedStyle(elementRef.current);
setDimensions({
// Position
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
// Size
width: rect.width,
height: rect.height,
// Offset dimensions (includes padding, border)
offsetWidth: elementRef.current.offsetWidth,
offsetHeight: elementRef.current.offsetHeight,
// Client dimensions (excludes scrollbar)
clientWidth: elementRef.current.clientWidth,
clientHeight: elementRef.current.clientHeight,
// Scroll dimensions
scrollWidth: elementRef.current.scrollWidth,
scrollHeight: elementRef.current.scrollHeight,
// Computed styles
marginTop: computedStyle.marginTop,
paddingLeft: computedStyle.paddingLeft,
});
}
};
measureElement();
// Remeasure on resize
window.addEventListener('resize', measureElement);
return () => window.removeEventListener('resize', measureElement);
}, []);
return (
<div>
<div ref={elementRef} style={{ padding: '20px', border: '2px solid' }}>
Measure me!
</div>
<pre>{JSON.stringify(dimensions, null, 2)}</pre>
</div>
);
}
Example: Scroll Position Tracking
function ScrollTracker() {
const containerRef = useRef(null);
const [scrollInfo, setScrollInfo] = useState({
scrollTop: 0,
scrollPercentage: 0,
isAtBottom: false
});
const handleScroll = () => {
if (containerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
const scrollPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
setScrollInfo({
scrollTop,
scrollPercentage: Math.round(scrollPercentage),
isAtBottom
});
}
};
const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
};
return (
<div>
<div>
<p>Scroll: {scrollInfo.scrollTop}px</p>
<p>Progress: {scrollInfo.scrollPercentage}%</p>
<p>At bottom: {scrollInfo.isAtBottom ? 'Yes' : 'No'}</p>
</div>
<div
ref={containerRef}
onScroll={handleScroll}
style={{ height: '300px', overflow: 'auto' }}
>
{/* Long content */}
</div>
<button onClick={scrollToBottom}>Scroll to Bottom</button>
</div>
);
}
Example: Intersection Observer with Refs
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
rootMargin: '50px', // Load slightly before visible
threshold: 0.01
}
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} style={{ minHeight: '200px' }}>
{isVisible && (
<img
src={src}
alt={alt}
onLoad={() => setHasLoaded(true)}
style={{ opacity: hasLoaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
)}
</div>
);
}
Example: ResizeObserver for Responsive Components
function ResponsiveCard({ children }) {
const cardRef = useRef(null);
const [size, setSize] = useState('medium');
useEffect(() => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width < 300) {
setSize('small');
} else if (width < 600) {
setSize('medium');
} else {
setSize('large');
}
}
});
if (cardRef.current) {
observer.observe(cardRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={cardRef} className={`card card-${size}`}>
{children}
</div>
);
}
| Observer API | Purpose | Callback Timing |
|---|---|---|
| IntersectionObserver | Detect when element enters/exits viewport | When visibility changes |
| ResizeObserver | Detect when element size changes | When dimensions change |
| MutationObserver | Detect DOM tree changes | When DOM mutates |
| PerformanceObserver | Monitor performance metrics | When performance entries recorded |
Performance Considerations:
- Use IntersectionObserver instead of scroll event listeners
- Use ResizeObserver instead of window resize + polling
- Debounce/throttle measurements on scroll/resize when necessary
- Read measurements in batches to avoid layout thrashing
- Cache measurements when possible (recompute only on dependency change)
- Disconnect observers when component unmounts
6. Third-party Library Integration with Refs
| Library Type | Integration Pattern | Common Examples |
|---|---|---|
| DOM Libraries | Pass ref.current to library initialization | D3.js, Chart.js, Three.js |
| jQuery Plugins | Initialize plugin on ref element in useEffect | DataTables, Select2, jQuery UI |
| Maps | Provide container ref for map initialization | Leaflet, Google Maps, Mapbox |
| Rich Text Editors | Mount editor instance to ref element | TinyMCE, Quill, Monaco |
| Video Players | Initialize player with ref element | Video.js, Plyr, JW Player |
| Canvas Libraries | Get canvas element via ref for rendering | Fabric.js, Konva.js, Paper.js |
Example: D3.js Chart Integration
import { useRef, useEffect } from 'react';
import * as d3 from 'd3';
function D3Chart({ data }) {
const svgRef = useRef(null);
useEffect(() => {
if (!svgRef.current || !data) return;
// Clear previous content
d3.select(svgRef.current).selectAll('*').remove();
// Create chart
const svg = d3.select(svgRef.current);
const width = 500;
const height = 300;
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const x = d3.scaleBand()
.domain(data.map(d => d.label))
.range([margin.left, width - margin.right])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.nice()
.range([height - margin.bottom, margin.top]);
// Add bars
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.label))
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => y(0) - y(d.value))
.attr('fill', 'steelblue');
// Add axes
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
}, [data]);
return <svg ref={svgRef} width={500} height={300} />;
}
Example: Leaflet Map Integration
import { useRef, useEffect } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
function LeafletMap({ center, zoom, markers }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
if (!mapRef.current) return;
// Initialize map
if (!mapInstanceRef.current) {
mapInstanceRef.current = L.map(mapRef.current).setView(center, zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(mapInstanceRef.current);
}
// Cleanup
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
// Update markers
useEffect(() => {
if (!mapInstanceRef.current) return;
// Clear existing markers
mapInstanceRef.current.eachLayer((layer) => {
if (layer instanceof L.Marker) {
mapInstanceRef.current.removeLayer(layer);
}
});
// Add new markers
markers?.forEach((marker) => {
L.marker(marker.position)
.addTo(mapInstanceRef.current)
.bindPopup(marker.popup);
});
}, [markers]);
return <div ref={mapRef} style={{ height: '400px', width: '100%' }} />;
}
Example: Monaco Editor Integration
import { useRef, useEffect } from 'react';
import * as monaco from 'monaco-editor';
function CodeEditor({ value, language, onChange }) {
const editorRef = useRef(null);
const editorInstanceRef = useRef(null);
useEffect(() => {
if (!editorRef.current) return;
// Create editor
editorInstanceRef.current = monaco.editor.create(editorRef.current, {
value: value || '',
language: language || 'javascript',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: false }
});
// Listen to changes
const disposable = editorInstanceRef.current.onDidChangeModelContent(() => {
const newValue = editorInstanceRef.current.getValue();
onChange?.(newValue);
});
// Cleanup
return () => {
disposable.dispose();
editorInstanceRef.current?.dispose();
};
}, []);
// Update value from prop
useEffect(() => {
if (editorInstanceRef.current && value !== undefined) {
const currentValue = editorInstanceRef.current.getValue();
if (currentValue !== value) {
editorInstanceRef.current.setValue(value);
}
}
}, [value]);
// Update language
useEffect(() => {
if (editorInstanceRef.current && language) {
const model = editorInstanceRef.current.getModel();
monaco.editor.setModelLanguage(model, language);
}
}, [language]);
return <div ref={editorRef} style={{ height: '500px', width: '100%' }} />;
}
Example: jQuery Plugin Wrapper
import { useRef, useEffect } from 'react';
import $ from 'jquery';
import 'select2'; // jQuery plugin
import 'select2/dist/css/select2.css';
function Select2Component({ options, value, onChange }) {
const selectRef = useRef(null);
useEffect(() => {
if (!selectRef.current) return;
// Initialize Select2
const $select = $(selectRef.current);
$select.select2({
data: options,
width: '100%'
});
// Listen to changes
$select.on('change', (e) => {
onChange?.(e.target.value);
});
// Cleanup
return () => {
$select.select2('destroy');
$select.off('change');
};
}, [options]);
// Update value from prop
useEffect(() => {
if (selectRef.current && value !== undefined) {
$(selectRef.current).val(value).trigger('change');
}
}, [value]);
return (
<select ref={selectRef}>
{options.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.text}
</option>
))}
</select>
);
}
| Integration Challenge | Solution | Example |
|---|---|---|
| Library expects DOM element | Pass ref.current in useEffect after mount | D3.js, Leaflet initialization |
| Library manages own state | Sync library state with React state via callbacks | Editor onChange, map move events |
| Library mutates DOM | Don't render React children in library container | Keep library container div empty |
| Cleanup required | Call library destroy/dispose in useEffect cleanup | Monaco dispose(), Select2 destroy() |
| Props update | Update library instance in separate useEffect | Update map center, chart data |
| Instance methods needed | Store instance in ref, expose via useImperativeHandle | Map pan/zoom, editor formatting |
Third-party Integration Best Practices:
- Initialize library in useEffect with empty deps ([] = mount only)
- Store library instance in ref for method calls
- Always cleanup (destroy/dispose) in useEffect return
- Sync React state to library in separate useEffect with deps
- Don't let React and library both manage same DOM (choose one)
- Wrap in custom component to encapsulate integration logic
- Document which React patterns to avoid with library
- Consider using React wrapper libraries when available