WordPress Interactivity API: Reactive Blocks Without React

The WordPress Interactivity API (stable since WordPress 6.5) provides a directive-based reactivity system for blocks — similar to Alpine.js but fully integrated with Server-Side Rendering and the block editor. It replaces ad-hoc jQuery for block interactivity without requiring React on the front end.

Problem: Gutenberg blocks with interactive features — accordions, tabs, live filters — traditionally require writing separate vanilla JavaScript or a React-based approach that duplicates server-rendered state on the client.

Solution: Use the WordPress Interactivity API (stable since WordPress 6.5) — declare reactive state in a wp-interactive wrapper, bind it to DOM elements with data-wp-bind, data-wp-on, and data-wp-context directives, and handle actions with data-wp-on--click. No React or custom framework is needed.

The examples below build an interactive counter block using wp-context, wp-bind, and wp-on directives, and show how to share state between multiple blocks on the same page.

 0, 'step' => 1 ] );

// wp_interactivity_data_wp_context() serialises context for the directive
$context = wp_interactivity_data_wp_context( [ 'count' => 0 ] );
?>
data-wp-interactive="myCounter" >

The JavaScript store — registered once, works for every counter block instance on the page:

// view.js — enqueued as viewScriptModule in block.json
import { store, getContext, getElement } from '@wordpress/interactivity';

const { state, actions } = store( 'myCounter', {
    state: {
        // Derived state: computed from context
        get isAtMin() {
            return getContext().count <= 0;
        },
    },
    actions: {
        increment() {
            const context = getContext();
            context.count += state.step;  // state.step from wp_interactivity_state()
        },
        decrement() {
            const context = getContext();
            if ( context.count > 0 ) context.count -= state.step;
        },
    },
    callbacks: {
        // Runs whenever context.count changes — like a watcher
        logChange() {
            const { count } = getContext();
            console.log( 'Count changed to:', count );
        },
    },
} );

// Share state between blocks: import the same store namespace in both blocks
// const { state } = store( 'myCounter' ); — read-only access from another block

NOTE: The Interactivity API requires blocks to use apiVersion: 3 and declare "interactivity": true in block.json. Scripts are loaded as ES modules (viewScriptModule) so you get native import/export without a bundler for simple blocks.

Leave Comment

Your email address will not be published. Required fields are marked *