WordPress 7.0 is officially on the horizon, and while most of the marketing fluff will focus on shiny new UI features, the real meat for us developers lies in the Interactivity API. I’ve spent enough time wrestling with race conditions in custom Gutenberg blocks to know that when Core refines its state management, we need to pay attention.
In this update, we’re seeing a shift toward better programmatic control. Specifically, the introduction of a new side-effect primitive and some long-overdue sanity checks for the router store. If you’ve ever tried to sync state between multiple stores or handle analytics without cluttering your DOM with directives, these changes are for you.
The New watch() Function: Programmatic Side Effects
Until now, if you wanted to react to a state change in the Interactivity API, you were largely tied to the data-wp-watch directive on a specific DOM element. This worked for simple UI toggles, but it was a nightmare for high-level logic like logging, synchronizing independent stores, or handling complex side effects that didn’t “belong” to a single HTML tag.
WordPress 7.0 introduces the watch() function to the @wordpress/interactivity package. It allows you to subscribe to reactive values programmatically. It runs immediately upon initialization and re-runs every time a dependency inside the callback changes.
import { store, watch } from '@wordpress/interactivity';
const { state } = store( 'bbioon/myPlugin', {
state: {
counter: 0,
},
} );
// This runs immediately and whenever state.counter updates.
const unwatch = watch( () => {
console.log( `The current count is: ${ state.counter }` );
} );
// If you need to stop watching:
unwatch();
One “war story” gotcha: keep an eye on your cleanup. Just like React’s useEffect, the callback in watch() can return a function to clean up listeners or timers before the next run. Use it, or you’ll end up with memory leaks that are a pain to debug in production.
Router Store Refinements: No More Race Conditions
The core/router store has always been a bit “messy” regarding initialization. Previously, state.url was initialized on the client side. This meant the value was undefined until the JS module finished loading asynchronously. We all had to write those annoying if ( ! state.url ) return; guards to prevent our logic from firing prematurely.
Starting in WordPress 7.0, state.url is populated on the server during directive processing. This is a massive win for reliability. Now, you can combine watch() with state.url to track virtual page views for analytics without worrying about that initial “undefined” state being treated as a navigation event.
import { store, watch } from '@wordpress/interactivity';
const { state } = store( 'core/router' );
watch( () => {
// Reliably track every client-side navigation.
bbioon_send_analytics( state.url );
} );
Furthermore, the internal properties state.navigation.hasStarted and state.navigation.hasFinished are now deprecated. These were always meant to be internal implementation details for the loading bar, and WordPress 7.1 will likely introduce a more robust public API for tracking navigation states.
For more on how these updates impact existing projects, check out my deep dive on Interactivity API refinements and the official WordPress 7.0 dev notes.
Look, if this 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: Ship Faster with Native Primitives
The addition of watch() and the server-side population of the router state are exactly the kinds of “quality of life” improvements we need. It makes the API feel less like a collection of DOM hacks and more like a proper reactive framework. Refactor your navigation tracking now to avoid the console warnings coming in 7.1.