React

Rendering React inside the WordPress block editor without losing your mind

Gutenberg ships its own pinned React. Here's the mental model Earle uses to mount React islands in the block editor without the two worlds fighting over the DOM.

edit.js
import { useRef, useEffect } from '@wordpress/element';
import { createRoot } from 'react-dom/client';
import Chart from './Chart';

export default function ChartBlockEdit() {
  const containerRef = useRef(null);

  useEffect(() => {
    const root = createRoot(containerRef.current);
    root.render(<Chart />);
    return () => root.unmount();
  }, []);

  return <div ref={containerRef} />;
}

Every few months someone asks me the same question: “Gutenberg is React, so why can’t I just drop my React app into a block?” You can. It’s just that the editor has its own ideas about when to re-render, and if you don’t respect them, your component will flicker, double-mount, or quietly leak.

The good news is that the fix is mostly a mindset. Once you stop treating the block editor like an empty div and start treating it like a host application you’re a guest inside, the patterns fall into place. Here’s how I think about it.

Gutenberg already runs React, so use its copy

WordPress bundles a specific version of React and exposes it as @wordpress/element. If you import react directly and bundle your own copy, you’ll ship two Reacts to the same page. Hooks break in confusing ways the moment a component crosses that boundary.

So the first rule is simple: import from @wordpress/element, not react, and mark React as external in your build. You get the editor’s copy, the hooks line up, and your bundle gets smaller for free.

Rule of thumb: If a dependency touches React, it has to use the same React the editor is using. One copy per page, no exceptions.

Your block’s edit function is already a React component

This is the part people miss. The edit function you pass to registerBlockType isn’t a place to bootstrap React. It is React, rendered by the editor on every keystroke, selection change, and attribute update. Most of the time you should just return JSX and let it re-render.

You only need to reach for an imperative mount when you’re wrapping something that isn’t React-aware: a charting library, a map, a third-party widget that wants a raw DOM node.

// Wrapping a non-React widget the right way
export default function Edit({ attributes }) {
  const ref = useRef(null);

  useEffect(() => {
    const chart = renderChart(ref.current, attributes.data);
    return () => chart.destroy();   // clean up on unmount
  }, [attributes.data]);

  return <div ref={ref} className="swp-chart" />;
}

The cleanup return is the whole game. The editor mounts and unmounts your block constantly as the user moves around the canvas. Without that teardown, every move leaks a chart instance and a handful of event listeners.

Keep the editor view and the saved view honest

A block has two lives: what it looks like in the editor, and what gets saved to the database as markup. If your React island only exists in the editor, you need a matching front-end script that finds the same markup on the published page and hydrates it.

I keep those two entry points side by side so they never drift:

  • edit.js renders the interactive editor experience.
  • save.js writes a predictable wrapper element with the attributes serialized onto it.
  • view.js runs on the front end, finds those wrappers, and mounts the same component.

Treat the saved markup as a contract. The front end should be able to rebuild the component from nothing but the HTML in the page.

When in doubt, mount less

The teams that get into trouble are the ones who try to turn the whole editor into their app. You almost never need that. A page is mostly server-rendered HTML with a few interactive islands sprinkled in, and the editor is happiest when you think the same way.

Render small, clean up after yourself, and borrow the editor’s React instead of bringing your own. Do that and the block editor stops feeling like a fight and starts feeling like a pretty reasonable host.

Earle

Senior WordPress Developer · Sarasota, FL

I've spent 20 years making WordPress behave like a real application platform. I write here about the patterns that survive production and flag the ones I ended up ripping out after they looked fine in a demo.