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.

<?php
// render.php — server-side render of the interactive counter block
// wp_interactivity_state() sets the initial server-side state
wp_interactivity_state( 'myCounter', [ 'count' => 0, 'step' => 1 ] );

// wp_interactivity_data_wp_context() serialises context for the directive
$context = wp_interactivity_data_wp_context( [ 'count' => 0 ] );
?>
<div
    <?php echo get_block_wrapper_attributes(); ?>
    data-wp-interactive="myCounter"
    <?php echo $context; ?>
>
    <button
        type="button"
        data-wp-on--click="actions.decrement"
        data-wp-bind--disabled="state.isAtMin"
        aria-label="<?php esc_attr_e( 'Decrease', 'myplugin' ); ?>"
    >−</button>

    <span data-wp-text="context.count"></span>

    <button
        type="button"
        data-wp-on--click="actions.increment"
        aria-label="<?php esc_attr_e( 'Increase', 'myplugin' ); ?>"
    >+</button>
</div>

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 *