skip to content

How would you integrate a third-party library that creates and owns its own DOM — a charting library, a map, a rich-text editor — into a React function component?

level: middleimportance: must knowfreq 60%

answer

  1. one node marks the ownership boundary
  2. container empty, library owns inside
  3. instance lives in a ref
  4. setup and cleanup must mirror exactly
  5. props go through setters, not the constructor

basics

~20 s

Render an empty container element with a ref, create the library instance in an effect from that node, keep the instance in a ref, and destroy it in the effect's cleanup. React renders no children inside that container.

solid answer

~50 s

I draw a hard ownership line: React owns one container element, the library owns everything inside it. The component renders `<div ref={containerRef} />` with no children. An effect with an empty dependency array constructs the instance from `containerRef.current`, stores it in a second ref, and returns a cleanup that calls the library's `destroy` or `remove` and nulls that ref — so setup and teardown are exact mirrors. Props are not passed through the constructor on every change; separate effects watch each prop and call the library's imperative setters on the stored instance. The cleanup is not optional politeness: it is what makes the component survive StrictMode's development remount and any real remount, and what stops listeners, observers and animation loops from piling up. If React content has to appear inside library-owned DOM, it goes through `createPortal` into a node the library created.

code

javascript · 21 lines
javascript
import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

export function MapView({ center, zoom }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    const map = L.map(containerRef.current).setView(center, zoom);
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
    mapRef.current = map;

    return () => {
      map.remove();
      mapRef.current = null;
    };
  }, []);

  return <div ref={containerRef} style={{ height: 400 }} />;
}

go deeper

for a junior

Be able to sketch the skeleton from memory: empty div with a ref, construct inside useEffect with empty deps, keep the instance in a second ref, destroy it in the returned cleanup.

for a middle

Explain why each piece is there — why the effect and not the body, why a ref and not state, why the deps are empty — and why the container has to stay free of React-rendered children.

for a senior

Talk about what goes wrong at scale: incomplete teardown surfacing as duplicate instances and detached-DOM leaks, construction-only options handled by remounting with a key, and library modules that break server rendering at import time.

for a principal

Own the pattern rather than the instance: a single reviewed wrapper convention, plus a rule about which libraries earn a React wrapper at all versus being replaced by something that speaks React natively.

## The division of labour A chart, a map, or an editor is an imperative object: you construct it against a DOM element, it fills that element with markup it manages itself, you call methods to change it, and you call a destructor when you are done. React is declarative: it wants to own a tree of nodes and diff them against a description you return. The two models can coexist only if you draw a boundary, and the boundary is one DOM node. React owns the container element and nothing below it. The library owns everything inside. Every rule below follows from that sentence. ## The skeleton ```js function MapView({ center, zoom }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { const map = L.map(containerRef.current).setView(center, zoom); mapRef.current = map; return () => { map.remove(); mapRef.current = null; }; }, []); return <div ref={containerRef} style={{ height: 400 }} />; } ``` Four moving parts: a container ref so the effect can find *this* instance's node; an effect that runs after the commit, when the node is attached; an instance ref so other effects and handlers can reach the object; and a cleanup that undoes exactly what setup did. ## Why a ref for the instance, not state The instance is not rendered data. Nothing in the returned JSX depends on it, so writing it to state would schedule a re-render that changes no output. Refs are also readable and writable synchronously across renders, which is what the cleanup and the prop-sync effects need. State would additionally be a render-ordering hazard: you would have to render at least once with `null` before any other effect could use it. ## The dependency array Empty deps express "this is a lifecycle, not a reaction": create when the component mounts, destroy when it unmounts. Construction options read inside that effect are captured at mount, which is correct for options the library only honours at construction time — but it does mean later changes to them are ignored, so anything the user can change afterwards must be fed through an imperative setter in its own effect. If an option genuinely can only be set at construction and genuinely has to change, do not smuggle it into the deps: give the wrapper a `key` derived from it, so React unmounts the old component and mounts a new one, running your existing teardown and setup instead of a bespoke rebuild path. ## The container stays empty Render no children inside the container. React diffs the children of the nodes it created against its own model of them, and library-inserted nodes are not in that model, so a React update can remove or reorder the wrong things. If the component needs a heading, a spinner or a toolbar, render those as **siblings** of the container. If React content truly must live inside library-owned markup — a popup body, a cell renderer — use `createPortal` targeting a node the library created and gave you. A robust variant is to have the effect create its own child node, append it to the container, hand *that* to the library, and remove it in cleanup. React then only ever owns an empty outer div, and teardown is guaranteed to leave that div genuinely empty even if the library's destructor is sloppy. ## Cleanup is the whole contract The cleanup must reverse everything the setup did: destroy the instance, drop the reference, and remove anything the library appended outside the container (some libraries attach tooltips or overlays to `document.body`). Two things depend on this being complete. In development, StrictMode runs setup, cleanup, then setup again on mount, which turns incomplete teardown into an immediately visible duplicate. In production, any real remount — a route the user navigates back to, a changed `key`, a conditional branch flipping — does the same thing, and an instance that outlives its component keeps its listeners, timers and observers alive, holding its detached DOM in memory. ## Server rendering Effects do not run on the server, so the initialization is client-only for free: the server emits an empty container, and the library fills it after hydration. What is not free is the library's *module*: if it touches `window` at import time, import it dynamically inside the effect rather than at the top of the file, and ignore the resolved module if the effect has already been cleaned up. ## The checklist a reviewer applies Container rendered by React and empty; ref, not a document query; construction inside an effect; instance in a ref; empty deps for the lifecycle effect; cleanup mirroring setup exactly; props applied through setters; no React children under library-owned DOM.

  • Why keep the library instance in a ref rather than in state?
    Because it is not rendered data. Nothing in the JSX depends on it, so putting it in state schedules a re-render that changes nothing, and forces an extra render pass with a null value before other effects can use it. A ref is mutable across renders and readable synchronously, which is exactly what cleanup and the prop-sync effects need.
  • What belongs in the initialization effect's dependency array, and what if a construction-only option has to change?
    An empty array — the effect is a lifecycle, not a reaction. If an option can only be set at construction and genuinely changes, do not add it to the deps and invent a rebuild path; give the wrapper component a `key` derived from that option. React then unmounts and remounts it, reusing the cleanup and setup you already wrote and trust.
  • The library appends a tooltip layer to document.body. Does your cleanup change?
    Yes. Cleanup must reverse everything setup caused, not just what lives in the container. If the library's own destructor removes its body-level nodes, calling it is enough; if it does not, remove them yourself in the cleanup. Otherwise every mount leaves an orphan layer behind, which shows up as stacked tooltips and a growing body child list.

saying these in an interview costs you the question

  • Renders React children inside the library's container
  • Passes props through the constructor by adding them to deps
  • Stores the instance in state and re-renders for nothing
  • Skips destroy because 'React removes the div anyway'
  • Disables StrictMode to stop the duplicate instance

context