BuilderJS

How to Embed a Drag-and-Drop Builder in React

BuilderJS is a framework-agnostic, vanilla-JavaScript drag-and-drop builder for HTML email and pages that you embed directly inside your own React app. It is the editor you own and self-host, not a SaaS — there is no monthly fee, no per-seat pricing, and no hosted backend to depend on. Because the engine ships as a global browser script that attaches window.Builder, embedding it in React is mostly about giving it a stable place to mount. You render three container divs in your JSX, then instantiate the builder once inside a useEffect after the DOM exists. There is no React component to npm install and no module export to import — you load the bundle's script and stylesheet, and the constructor reads CSS selector strings to find your containers. This page walks through the exact mount pattern, including the React 18 Strict-Mode guard, and how to save and restore designs with getData() and load(). Everything here matches the real BuilderJS API so you can copy, paste, and ship.

Mount it

import { useEffect, useRef } from 'react';

// builder.js + builder.css are loaded globally (e.g. in index.html),
// which attaches window.Builder. There is NO npm import.
export default function EmailBuilder() {
  const mounted = useRef(false);

  useEffect(() => {
    if (mounted.current) return; // guard React 18 Strict-Mode double-invoke
    mounted.current = true;

    const builder = new window.Builder({
      mainContainer: '#bjs-canvas',       // REQUIRED — CSS selector string
      widgetsContainer: '#bjs-widgets',   // optional — CSS selector string
      settingsContainer: '#bjs-settings', // optional — CSS selector string
    });

    // load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 args
    builder.load(window.THEME_JSON, window.THEME_TEMPLATES, window.THEME_CONFIG_DATA, window.MEDIA_URL);
    // No cleanup / destroy() — none exists; React discards the DOM on unmount.
  }, []);

  return (
    <>
      <div id="bjs-widgets"></div>
      <div id="bjs-canvas"></div>
      <div id="bjs-settings"></div>
    </>
  );
}

It's a global window.Builder script, not an npm import

BuilderJS is delivered as a compiled browser bundle, not an ES module. You load it with two tags — <link rel="stylesheet" href="/builderjs/builder.css"> and <script src="/builderjs/builder.js"></script> — and the script attaches a global named window.Builder. Add both to your index.html (or inject the script before mount); the path is just wherever you self-host the bundle. Do not write import { Builder } from 'builderjs'; there is no named export and that line will fail. In React you reference the global directly: new window.Builder({...}). This keeps the editor framework-agnostic — the same bundle drops into React, Vue, Svelte, or a plain page unchanged.

Client-side DOM mount inside useEffect

BuilderJS is a DOM-mount editor and is client-side only — it is never server-rendered. Mount it inside the framework's mount hook, which in React is useEffect(() => {...}, []) so it runs once after the component's DOM is in the document. Render the three container nodes in your JSX up front: <div id="bjs-widgets">, <div id="bjs-canvas">, and <div id="bjs-settings">. Then construct with new window.Builder({ mainContainer: '#bjs-canvas', widgetsContainer: '#bjs-widgets', settingsContainer: '#bjs-settings' }). Note these options are CSS selector strings — not DOM nodes, not refs. mainContainer is required (it resolves the canvas); widgetsContainer and settingsContainer are optional, so a selector that resolves to nothing simply leaves that sub-panel unmounted. If you use Next.js or another SSR framework, make sure the editor only renders on the client (e.g. a dynamic import with ssr disabled), because the bundle expects a live DOM.

Guard the React 18 Strict-Mode double-mount — there is no destroy()

In development, React 18 Strict Mode intentionally invokes effects twice to surface side-effect bugs. The Builder instance exposes no destroy(), teardown(), or dispose() method, so you cannot "clean up" between the two invocations — and you should not try. Instead, guard with a simple ref flag so the constructor runs exactly once: const mounted = useRef(false); if (mounted.current) return; mounted.current = true;. Leave the useEffect cleanup empty (or return nothing). On a real unmount, React discards the container DOM node and the editor goes with it; there is genuinely nothing to dispose. Never call builder.destroy() — that method does not exist.

Persistence with getData() and load()

BuilderJS does not send, host, or publish anything — it is the editor only, and you bring your own storage, sending, and hosting. To save a design, call const json = builder.getData() and POST that JSON to your backend. To restore it, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — exactly four positional arguments, in that order: the saved theme JSON, the theme templates JSON, the theme config data, and the media base URL. Passing your previously saved getData() output back through load() reconstructs the exact design. This clean split means BuilderJS stays a focused editor while you keep full control of where designs live and how finished email or pages get delivered.

Own your source with a one-time license

BuilderJS is sold under a one-time CodeCanyon license (Regular $52 / Extended $99) — you get the source, self-host the bundle, and embed it in as many of your own React screens as the license tier allows. There is no SaaS subscription, no monthly bill, and no per-seat charge. Because you serve builder.js and builder.css from your own origin, the editor has no external runtime dependency on a vendor service, which keeps your app fast, private, and fully under your control.

Frequently asked questions

Explore