WordPress 7.0 is just around the corner, and it brings some much-needed refinements to the WordPress 7.0 Interactivity API. If you’ve spent any time building complex reactive blocks, you know that while the directive-based approach is great for DOM-heavy interactions, it often felt limiting when you needed to run logic independently of a specific element.
I’ve definitely had those “war story” moments where I was trying to sync state between two different stores and ended up with a mess of data-wp-watch attributes just to trigger a simple side effect. The new watch() function finally addresses this programmatic gap, and it’s a game changer for stability.
The New Programmatic watch() Function
Previously, if you wanted to react to a state change, you were tethered to the lifecycle of a DOM element via data-wp-watch. This was fine for UI changes, but terrible for things like logging, analytics, or store-to-store synchronization. In the WordPress 7.0 Interactivity API, we now have a programmatic watch() function available in the @wordpress/interactivity package.
import { store, watch } from '@wordpress/interactivity';
const { state } = store( 'myPlugin', {
state: {
counter: 0,
},
} );
// This runs immediately and re-runs whenever state.counter changes.
const unwatch = watch( () => {
console.log( 'Counter is currently: ' + state.counter );
// You can also return a cleanup function
return () => {
console.log( 'Cleaning up before next run...' );
};
} );
// When you're done, just call the returned function to stop the watcher
// unwatch();
This is significantly cleaner than the old hacky ways of attaching invisible divs just to use a directive. If you’re looking to dive deeper into how this evolved, check out my previous post on Interactivity API: Extending Core Blocks Without the Bloat.
Fixing the state.url Race Condition
One of the most frustrating bottlenecks in earlier versions was how state.url handled initial page loads. Because the router module loaded asynchronously, state.url would often be undefined for a split second before snapping to window.location.href. This forced us to write defensive “guard” code everywhere to avoid reacting to that initial initialization as if it were a new navigation.
Starting with WordPress 7.0, state.url is now populated on the server. This means the value is ready as soon as the client-side script initializes. Combined with watch(), tracking virtual page views becomes trivial:
import { store, watch } from '@wordpress/interactivity';
const { state } = store( 'core/router' );
watch( () => {
// This reliably runs on every client-side navigation,
// without the initial "undefined" headache.
bbioon_send_to_analytics( state.url );
} );
Deprecations: Cleaning up core/router
We also need to talk about some house cleaning. The state.navigation.hasStarted and state.navigation.hasFinished properties in core/router are now officially deprecated. They were always internal implementation details for the loading bar, but developers (myself included) often reached for them because there wasn’t a better option.
If you’re using these, you’ll start seeing console warnings in SCRIPT_DEBUG mode. WordPress 7.1 is expected to bring a formal navigation state mechanism, but for now, you should refactor your logic to rely on the server-populated state.url and watch() mentioned above. For more on handling these transitions, see smooth client navigation with Interactivity API.
Look, if this WordPress 7.0 Interactivity API stuff is eating up your dev hours, let me handle it. I’ve been wrestling with WordPress since the 4.x days.
Takeaway
The WordPress 7.0 Interactivity API updates are moving in the right direction—away from DOM-dependency and towards a more robust reactive framework. By leveraging watch() and the improved server-side state, you can eliminate race conditions and keep your front-end logic clean. Just make sure to audit your router store usage before those deprecations turn into breaking changes.