close up photo of codes
, , ,

A Starter Plugin to Extend Elementor with React Components

This article is here to offer a solution, or at least one way of leveraging React to expand and explore possibilities in synergy with Elementor page builder.

Get the starter Elementor/React extension plugin

For those impatient, who like to get their hands on the code, and start working with it immediately, here’s the link to GitHub repo with “Elementor React Widgets” starter:

Get it here

After downloading the repo and unzip, open terminal (or VS Code) in unzipped dir and run npm install for all dependencies, and then npm run dev or npm run watch and start adding your React widgets! Run npm run build:prod for production ready plugin.
Or, open your terminal and run these commands:

git clone https://github.com/Micemade/elementor-react-widgets.git \
&& cd elementor-react-widgets \
&& npm install

When developing:

npm run build
npm run watch

Production build:

npm run build:prod

For those not so impatient, keep on reading to learn more, you can always scroll up here to get the starter plugin.

Motivation

Elementor and React are both very popular technologies, with vast audience and eco-systems, and both are being used by many to create stunning websites. Their popularity differs, the former is popular among users, agencies, and generally no-coders, the latter is very popular (for quite some time now) mostly in developers’ world.

As I am using React libraries and components (and creating my own), and I’ve created a lot of Elementor related stuff (websites, extension plugins, templates) I thought it might be good to combine those two popular techs FTW. But, (as I learned quickly) the tech and concepts behind React and Elementor differ in some amount, and integration can be tricky.

To overcome that, and make things simpler for me (and for you) I’ve created a starter plugin that bridges the gap between the big E and R, and makes the integration easier. This plugin (I called it creatively Elementor React Widgets) will enable you to:

  • Reuse React components: Take components you’ve built for other projects (like a product grid or interactive form) and drop them into Elementor without rewriting everything. Some adaptation will be required, though.
  • Leverage React’s power: Use hooks, state management, and modern UI patterns you’re already familiar with, instead of learning Elementor’s specific ways. Of course, some basic knowledge about Elementor “ways” and methods will be needed.
  • Avoid common pitfalls: Handle live updates in the editor without the annoying “flicker” (where the component reloads, or remounts, on every change), and ensure settings sync smoothly between Elementor and React.

This pattern saves time and reduces frustration by making React and Elementor work together seamlessly. Devs, your clients, your no-code colleagues, you bosses will love you when you bring all those React goodies into their favorite page builder 🙂

Video showing seamless integration of React component in Elementor and two way data communication.

What this starter solves

  • Seamless mounting: Mount React components inside Elementor widgets, working in both the live site and editor preview.
  • Flicker-free updates: Update your widget’s settings live without remounting the React component—smooth changes for your custom controls, while still allowing Elementor’s core settings (like spacing or layout) to apply instantly.
  • Two-way syncing: Keep Elementor and React in sync, so changes in one reflect in the other automatically.
  • Clear patterns: Provides reusable code for initializing, managing, and updating widgets, making it easy to add new ones.

High-level architecture

  • PHP widget: Defines Elementor controls and outputs a simple wrapper with a hidden JSON input (for initial settings) and a `div` as the React root.
  • Widget initializer: Finds the wrapper, reads settings, and tells the widget manager to mount or update the React component.
  • Widget manager: A central hub that creates React roots, tracks instances by `${widgetType}_${widgetId}`, and updates components in-place without remounting.
  • Elementor hooks: Sets up hooks for frontend and editor, conditionally prevents DOM re-renders (only for your widget’s settings, not core ones), and connects editor models for live syncing.
  • MutationObserver: In the editor, watches for new widgets added via drag-and-drop and initializes them automatically.

Key code examples

1) PHP:content_template()pattern for editor preview (simplified)

<# const data = { title: settings.title }; const jsonData = JSON.stringify(data); #>
<div class="my-widget-wrapper" data-widget-id="{{ view.model.id }}">
    <input type="hidden" class="elementor-settings-data" value="{{ jsonData }}" />
    <div class="my-widget-react-root"></div>
</div>

This outputs a data-widget-id and a hidden elementor-settings-data input that the initializer can parse on mount.

2) JS: Register frontend hook (initializer factory pattern)

// registerFrontendHooks
getRegisteredWidgets().forEach((widgetType) => {
  elementorFrontend.hooks.addAction(
    `frontend/element_ready/${widgetType}.default`,
    createWidgetInitializer(widgetType)
  );
});

3) JS: Simplified initializer: find root, parse JSON, fallback to editor model

const createWidgetInitializer = (widgetType) => {
  return ($scope) => {
    const wrapper = $scope.find(`.${widgetType}-wrapper`)[0] || $scope[0]?.querySelector(`.${widgetType}-wrapper`);
    const root = wrapper?.querySelector(`.${widgetType}-react-root`);
    const settingsInput = wrapper?.querySelector('.elementor-settings-data');
    // ...
    let settings = {};
    if (settingsInput?.value) settings = JSON.parse(settingsInput.value);
    const widgetId = wrapper?.dataset?.widgetId || $scope.data('id');
    widgetManager.init(widgetType, widgetId, root, settings);
  };

4) JS: Widget manager (update without remount, in src/core/widget-manager.jsx)

const instances = {};

const init = (widgetType, widgetId, rootElement, settings) => {
  const key = `${widgetType}_${widgetId}`;
  if (!instances[key]) {
    const root = createRoot(rootElement);
    const api = { settings };
    root.render(<WidgetRoot api={api} />);
    instances[key] = { root, api };
  }
};

const updateInstance = (widgetType, widgetId, newSettings) => {
  const key = `${widgetType}_${widgetId}`;
  const instance = instances[key];
  if (instance) {
    // Update settings in-place
    instance.api.settings = newSettings;
    instance.api.onSettingsChange?.(newSettings);
  }
};

5) JS: Settings change listener (Elementor → React)

model.get('settings').on('change', (settingsModel) => {
  const changed = Object.keys(settingsModel.changed || {});
  widgetManager.updateInstance(widgetType, model.id, mapSettingsFromModel(model), changed);
});

6) JS: Editor hooks — conditionally prevent DOM replacement and register model getter

// In registerEditorHooks: Hook into panel opening for each widget type
getRegisteredWidgets().forEach((widgetType) => {
  elementor.hooks.addAction(`panel/open_editor/widget/${widgetType}`, (panel, model, view) => {
    // Store model getter for live settings access
    widgetManager.modelGetters[`${widgetType}_${model.id}`] = () => mapSettingsFromModel(model);
    widgetManager.models[`${widgetType}_${model.id}`] = model; // For two-way sync

    // Conditionally override renderOnChange:
    // - prevent re-render for widget-owned settings,
    // - allow for core/advanced settings (e.g.widget margins, layout etc.)
    if (view && typeof view.renderOnChange === 'function') {
      const widgetConfig = getWidgetConfig(widgetType);
      const widgetKeys = Object.keys(widgetConfig.settingsMapper(model) || {});
      const originalRenderOnChange = view.renderOnChange.bind(view);

      view.renderOnChange = (settings) => {
        const changed = settings.changedAttributes();
        const hasNonWidgetChange = Object.keys(changed).some(k => !widgetKeys.includes(k));
        if (hasNonWidgetChange) {
          // Allow Elementor to re-render for core/advanced settings
          originalRenderOnChange(settings);
        }
        // Skip re-render for widget-owned changes (React handles them smoothly)
      };

      widgetManager.registerView(widgetType, model.id, view);
    }

    // Push initial settings to React
    widgetManager.updateInstance(widgetType, model.id, widgetManager.modelGetters[`${widgetType}_${model.id}`]());
  });
});

Practical notes and gotchas

  • Editor flicker: This happens when Elementor replaces the entire widget’s DOM on every setting change, causing React components to unmount and remount. We solve this by conditionally overriding `view.renderOnChange` in the panel hook: it returns `false` (no re-render) for changes to your widget’s own settings (handled smoothly by React), but allows re-rendering for core/advanced settings like margins or layout (so they apply live in the editor).
  • Initial data path: Use a hidden JSON input in `content_template()` for frontend reliability. In the editor, rely on the stored `modelGetter` to access live model state.
  • Two-way sync: Store the Elementor `model` in the widget manager so React can update the editor model (e.g., when a control inside React changes a value).
  • Stateful components: By avoiding remounts for widget settings, local React state (like form inputs or animations) stays intact, providing a better user experience.
  • Conditional logic: The key is distinguishing “widget-owned” settings (e.g., your custom title or color) from “core/advanced” ones (e.g., padding or background). Use the `settingsMapper` to define widget-owned keys, and let Elementor handle the rest.

Developer workflow

  • Build: Vite bundles JS into `assets/js/main.js`. A post-build step extracts CSS into `assets/css/style.css` so the plugin can output a stylesheet for the frontend.
  • Formatting & linting: Prettier + ESLint recommended. Consider `lint-staged` + Husky to run Prettier on commit.

How to add a new widget (summary)

  1. Create `widgets/my-widget.php` with register_controls(), content_template() (hidden JSON + react root), and render().
  2. Add a React component in `src/widgets/my-widget/` and a `settings-mapper` that maps Elementor model to a plain JS object.
  3. Register my-widget in `src/core/widget-registry.js` with component + mapper.
  4. Ensure the SCSS for the widget is compiled and available in `assets/css/style.css`.

Final thoughts

This starter plugin shows how to blend React’s flexibility with Elementor’s visual editing, creating a smooth experience for both developers and content creators. By conditionally handling updates—letting React manage your custom settings while Elementor handles the rest—you get the best of both worlds: no flicker, live previews, and reusable components.


Post tagged with:

Comments

Leave a Reply

Discover more from Micemade

Subscribe now to keep reading and get access to the full archive.

Continue reading