React

Sharing state between React islands without a global store

Two islands, one page, no Redux. How to keep independent React roots in sync using @wordpress/data, custom events, and the occasional shared context.

Context only flows inside a single React tree. If you mount two createRoot calls, you need @wordpress/data or custom events to bridge them.
The question that decides your state strategy on multi-island WordPress pages

You finally got React islands working on a WordPress page. One handles a filter panel, another renders results, maybe a third shows a live preview. Then someone asks a reasonable question: when the user picks a category in island A, how does island B know about it?

The instinct is to reach for Redux, or Zustand, or some global store because that is what we do in greenfield SPAs. On a WordPress page you are usually a guest. You might have two or three small roots mounted into template partials, a theme that loads jQuery plugins you cannot remove, and no guarantee that every island even loads on every page view. A global store is not wrong, but it is often more machinery than the problem deserves.

Start by asking what actually needs to stay in sync

Before you pick a pattern, write down what state is shared and who owns it. I use a quick three-column sketch: source of truth, readers, and writers. If one island owns the data and the others only display it, you need a notification path, not necessarily a shared store. If three islands read and write the same fields, you need something centralized.

Most WordPress admin screens I work on already have a centralized option: @wordpress/data. The block editor and modern settings pages use it everywhere. If your islands live inside wp-admin and touch WordPress entities, start there before inventing anything custom.

@wordpress/data when you are already in the WordPress admin

If both islands run inside an admin context where @wordpress/data is available, register a small custom store or extend an existing one. Your first island dispatches an action, the store updates, and the second island selects the slice it cares about. React re-renders happen through the normal @wordpress/data subscription path, which feels a lot like Redux but ships with WordPress and already understands REST resolvers and nonce handling.

// A tiny store for a shared UI flag
const store = createReduxStore( 'my-plugin/ui', {
	reducer( state = { filter: '' }, action ) {
		if ( action.type === 'SET_FILTER' ) {
			return { ...state, filter: action.payload };
		}
		return state;
	},
	actions: {
		setFilter( filter ) {
			return { type: 'SET_FILTER', payload: filter };
		},
	},
	selectors: {
		getFilter( state ) {
			return state.filter;
		},
	},
} );

register( store );

Both islands import useSelect and useDispatch from @wordpress/data. No custom event wiring, no second copy of React, and if you later add a third island it plugs into the same store without a rewrite.

The catch is scope. @wordpress/data is the right default inside Gutenberg and modern admin SPAs. It is not loaded on a public theme page unless you enqueue it, and you probably should not enqueue the entire data package on the front end just to sync two widgets.

Custom events for front-end islands that stay lightweight

On the public site, my default for two or three islands is a thin event bus built on CustomEvent. One island dispatches a namespaced event on window or a shared DOM node, the others listen in a useEffect and update local state. Type the detail object once in TypeScript and treat it like a contract.

// Island A: user changed the filter
window.dispatchEvent(
	new CustomEvent( 'myplugin:filter-change', {
		detail: { category: 'news' },
	} )
);

// Island B: subscribe once on mount
useEffect( () => {
	function onFilterChange( event ) {
		setCategory( event.detail.category );
	}
	window.addEventListener( 'myplugin:filter-change', onFilterChange );
	return () =>
		window.removeEventListener( 'myplugin:filter-change', onFilterChange );
}, [] );

This pattern is easy to debug in DevTools, works across islands that do not share a build, and does not care if one island hydrated late because the listener attaches on mount. The failure mode is discipline: if everyone dispatches ad hoc events with different names, you get spaghetti. Pick a prefix, document the events in one file, and keep payloads flat.

When React context is enough (and when it is not)

Context works beautifully inside a single React tree. If your islands are really one app split into lazy chunks, mount one root and use context normally. The mistake I see is mounting two createRoot calls and expecting context to leak across them. It will not.

If you control the mount point and the islands are always siblings under one wrapper, do that. You get context, shared suspense boundaries if you use them, and one place to put error boundaries. If the theme places islands in unrelated template hooks, you are back to @wordpress/data or custom events.

Avoid the global window object unless you mean it

It is tempting to stash shared state on window.myPluginState because it is fast. I have done it under deadline pressure and regretted it when a caching plugin inlined a stale script order. If you go this route, wrap access in one module, never mutate from random theme files, and treat it as a last resort for legacy themes where events feel too fragile.

Picking a pattern in practice

For admin SPAs and Gutenberg-adjacent work, use @wordpress/data. For two or three front-end widgets in a theme you cannot refactor yet, use namespaced custom events. For a single cohesive app split across routes inside one mount node, use context and one root. Reach for a dedicated global store library when you have many writers, derived state, or time-travel debugging requirements that WordPress stores do not cover.

The goal is not the cleverest architecture. The goal is that when an editor changes a filter, the preview updates without a full page reload and without you maintaining three copies of the same string in three places. Boring wiring usually gets you there faster than importing another state library because it felt cleaner in a demo.

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.