I had a client with a slick e-commerce site they wanted to feel snappy. They were after a single-page application (SPA) feel, especially on the product filters and category pages. We built it on the WordPress Interactivity API for dynamic content loading, and for the basic stuff it worked great. Then they wanted a new interactive block, a complex product comparison table, with its own styles and scripts that would load dynamically whenever someone filtered products. Total nightmare, man.
My first thought was, “Fine, I’ll load the CSS and JS for these complex blocks myself.” So I hooked into the navigate action from @wordpress/interactivity-router and injected <link> and <script> tags into the head by hand. It sort of worked, but it was brittle: race conditions, duplicate styles, scripts not firing because dependencies were out of order. A band-aid that always felt one navigation away from tearing off. You end up debugging load order instead of building features.
Interactivity API client navigation actually works now
WordPress 6.9 fixed it. The core team finally dealt with the big client-side navigation problems in the Interactivity API. It no longer just swaps out the HTML, it updates the page’s dependencies too, which was exactly what we needed. The dev note on make.wordpress.org/core has the full technical breakdown.
Dynamic script modules and stylesheets are in
The biggest pain for me was new script modules and stylesheets. Before 6.9, client navigation only updated the HTML. A new block with its own JS or CSS would not load on its own. Now the @wordpress/interactivity-router module handles it: it loads new stylesheets, reuses the ones already on the page, and drops the ones that are no longer needed. Same for script modules and their dependencies, and new importmap definitions are supported too. Your interactive blocks load without the manual hacks.
- No more managing
<link>or<script>tags by hand. The router does it. - Prefetching now covers these dynamic assets, so you keep the “instant navigation” feel.
Router regions inside interactive elements
Another old gotcha was router regions. You mark an element with data-wp-router-region and the router updates its content. Simple, in theory. But these regions used to have to be direct children of a root interactive element. A nested interactive component with its own region just would not update, which forced some awkward DOM structures.
Not anymore. In 6.9, a router region updates as long as it sits inside any interactive region and carries its namespace through data-wp-interactive. That makes nested interactive components much easier to manage. The structure now looks like this:
<div data-wp-interactive="bbioon-example">
<button data-wp-on--click="actions.bbioonDoSomething">Click me!</button>
<div
data-wp-interactive="bbioon-example"
data-wp-router-region='bbioon-example/region-1'
>
I used to be stuck, now I update reliably on client navigation.
</div>
</div>
The new attachTo property for router regions
Sometimes you need a region on a page where it was not there to begin with, or outside the normal content area, like an overlay or a modal. WordPress 6.9 adds an attachTo property to the data-wp-router-region directive. Give it a CSS selector, and if the region is missing it gets rendered inside the element that matches.
<div
data-wp-interactive="bbioon-example"
data-wp-router-region='{ "id": "bbioon-example/region", "attachTo": "body" }'
>
I'm a new region, dynamically attached to the body!
</div>
That helps a lot with dynamic UIs, where a component might appear conditionally or need to render in a fixed spot on the page no matter where the activating block sits in the content.
Smarter getServerState and getServerContext
There are also real improvements to getServerState() and getServerContext(). These functions keep client-side interactivity in sync with server-rendered data. Before, resetting client-modified values back to the server defaults, or handling state that only existed on some pages, could get fiddly.
Now getServerState and getServerContext always trigger an invalidation, even when the server value has not changed, so you can reset client-side state reliably. Properties from a previous page are also removed if they do not exist on the current one, which keeps the client state matching what the server actually holds and stops stale data from carrying over between pages.
const { state } = bbioonStore( 'bbioonMyPlugin', {
// ...
callbacks: {
bbioonResetCounter() {
const serverState = bbioonGetServerState(); // Always { counter: 0 };
state.counter = serverState.counter; // Reset to 0;
},
},
} );
It is a small change, but it saves a lot of head-scratching when you are building interactive components that depend on server-side state.
So what’s the takeaway?
The Interactivity API in WordPress is powerful, but like any young tech it has rough edges. WordPress 6.9 files down some real ones, especially when you are going for a dynamic, SPA-like client navigation experience. These are not minor tweaks, they change the foundation you build interactive front-ends on and make it a lot less painful and more reliable.
This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want the site to work, drop my team a line. We’ve probably seen it before, and we know how to get the Interactivity API running smoothly.