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>
  );
}
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

Refs and DOM Manipulation Best Practices