Interactivity API

This article is part of the WordPress guide. Read the introduction.

The Interactivity API, introduced in WordPress 6.5, is a JavaScript framework built into WordPress. Its goal is to provide a standard and seamless way of adding frontend interactivity to blocks, without relying on jQuery or custom js. It’s very similar to Alpine.js, i.e., it’s a declarative framework extending standard HTML with custom data attributes (directives). These attributes are used to store state and define actions taken on given events.

This is a rather large topic, and it’s conceptually different from pretty much everything we’ve been discussing. Just imagine trying to learn a JavaScript framework in one chapter of a WordPress deep dive. Not going to happen. I’ll cover it enough so that you know how it works, and if you want to learn it more intimately, I suggest you read the official Interactivity API documentation.

Reactivity & Directives

The foundation of this API is reactivity. You don’t directly update the DOM. You declare the state the element depends on, and you declare actions taken on events that are responsible for changing that state. When the state changes, the elements react and update accordingly.

The most important abstract pieces of this system are:

  • State
    • Global state – global data shared and accessible by any element.
    • Local context (state) – local data accessible only by the particular element and its children.
    • Derived state – data computed dynamically from the global or local state.
  • Actions – functions, usually triggered by events, responsible for mutating state.
  • Bindings – HTML elements are bound to state and updated automatically when the state changes.
  • Side effects – optional callbacks executed when the state changes.

All of these parts are tied together using HTML data attributes, called directives. It’s very important you understand what a directive is. Take this element:

HTML
<p data-wp-interactive="thm/myBlock">abc</p>

‘data-wp-interactive’ is a directive. These are attributes starting with data-wp. We’ll talk about them without the data- prefix. Some of the most important attributes are:

  • wp-interactive – enables interactivity for the element and specifies its namespace.
  • wp-context – provides local context for the element.
  • wp-bind – binds an HTML attribute with a state.
  • wp-class – adds or removes a class based on a boolean value of a state.
  • wp-style – adds or removes inline style based on the value of a state.
  • wp-text – binds the inner text of the element with a state.
  • wp-on – runs an action on a specified event.
  • wp-watch – runs a callback when the state changes.

That’s only the beginning of a long list, but it’s all we need for a good understanding of this system. Consult the documentation to learn more. Look at this diagram of how the different parts of this system interact with each other. Maybe it will help you conceptualize it.

source: Interactivity API Reference

Code Example Using Local State

What better way to understand it than to actually use it, am I right? We’ll create a simple dynamic counter block. This block will consist of only 2 elements: a button increasing the counter, and a span displaying it. I will not show you any of the code needed to register and create the block. Only the relevant parts – mostly the block’s frontend HTML.

Here is this block’s render.php file. We use PHP to render the block because it’s a dynamic block:

PHP
<div
    data-wp-interactive="thm/counter"
    <?php
    echo get_block_wrapper_attributes();
    echo wp_interactivity_data_wp_context( array( 'counter' => 0 ) );
    ?>
>

    <button data-wp-on--click="actions.increment">Increment</button>
    <span data-wp-text="context.counter"></span>
</div>

Here is our code in the view.js file loaded on the frontend (you should load it using block.json):

JavaScript
import { store, getContext } from "@wordpress/interactivity";

store( 'thm/counter', {
    actions: {
        increment: () => {
            const context = getContext();
            context.counter += 1;
        },
    },
} );

And here is the final rendered HTML:

HTML
<div data-wp-interactive="thm/counter" data-wp-context='{ "counter": 0 }'>
<button data-wp-on--click="actions.increment">Increment</button>
<span data-wp-text="context.counter">0</span>
</div>

Okay, let’s decipher what’s going on here. First of all, the wp_interactivity_data_wp_context() function used in render.php is responsible for transforming an associative array into a formatted context directive. You could have just as well written data-wp-context yourself, but it’s better practice to use this function (it handles JSON encoding and sanitization of the attributes). Our local state – the context, is defined and stored only in this wp-context directive. We only have one state property – ‘counter’, with a default value of 0.

The view.js file loaded on the frontend creates a store object for the thm/counter namespace. A store is the highest-level abstract object in this API. It defines the global state, actions, and callbacks. We create an increment action, which, as you can see, is just a callback function. This function starts by getting the context of the element that called it using getContext(), and then increases the counter of this context.

The final HTML uses 4 directives. ‘wp-interactive’ activates the API and specifies the namespace. ‘wp-context’ defines the local state. ‘wp-on–click’ declares that the actions.increment action should be called on the ‘click’ event on the button. The naming convention is wp-on–{event}, so for other events you could do wp-on–input, wp-on–keyup, etc. Finally, ‘wp-text’ binds the ‘counter’ state to the span’s inner text.

So, what will happen when we click the button? Here’s the timeline:

  1. The ‘increment’ action will be executed, modifying the counter.
  2. The wp-context directive will be updated, with the ‘counter’ state being incremented by one.
  3. The API will automagically update the span’s inner text to the new value.

That’s it. We have successfully created an interactive counter element without a single getElementById() call. Explaining why a declarative approach is better than an imperative one is beyond the scope of this guide. Go read a few articles on modern js frameworks if you’re interested in learning more.

Code Example Using Global & Derived State

The previous example allowed us to place multiple counter blocks on the same page with each having its own counter. Let’s modify it to use global state – making one button update all of the counters on the page. You could imagine it being like a button for time travel. You’d want all of your clocks updated, not just one. 

Global state stores the data in the store instead of the inline context. The state should be initialized on the server. Here’s our new render.php file:

PHP
wp_interactivity_state( 'thm/counter', array(
    'counter' => 0,
) );

<div
    data-wp-interactive="thm/counter"
    <?php echo get_block_wrapper_attributes(); ?>
>

    <button data-wp-on--click="actions.increment">Increment</button>
    <span data-wp-text="state.counter"></span>
</div>

Here is the view.js:

JavaScript
import { store } from "@wordpress/interactivity";

const { state } = store( 'thm/counter', {
    actions: {
        increment: () => {
            state.counter += 1;
        },
    },
} );

And here is the final rendered HTML:

HTML
<div data-wp-interactive="thm/counter">
<button data-wp-on--click="actions.increment">Increment</button>
<span data-wp-text="state.counter">0</span>
</div>

The wp_interactivity_state() adds state to the global store object with the thm/counter namespace. We got rid of the wp-context directive, as we are not using local context anymore. Similarly, context.counter was changed to state.counter, which is a reference to the global state.

By the way, if you’ve been scratching your head wondering: “what the hell is a namespace?” – let me clear that up for you. Every plugin/author/element should define a namespace for their store to avoid name collisions. That doesn’t mean the store is only accessible to elements in this namespace. You can access another namespace’s counter by doing namespace::state.counter (in a directive).

That’s it. If we now placed multiple counter blocks on the same page, and incremented one of them, all of their counters would be incremented. That’s because they all share the same global state by referencing the ‘counter’ variable in the thm/counter store.

Let’s add more functionality to our block. How about we add a new span displaying double the value of the counter. You might be tempted to create a new state variable called counterDouble, but that would be wrong. If we did that, we’d have to remember to update the state twice in the action. We’re opening ourselves up for state synchronizations issues. Instead, we can use derived state.

Here is our updated view.js code:

JavaScript
import { store } from "@wordpress/interactivity";

const { state } = store( 'thm/counter', {
    state: {
        get double() {
            return state.counter * 2;
        },
    },
    actions: {
        increment: () => {
            state.counter += 1;
        },
    },
} );

Notice the new state object and a double() getter function defined inside. That’s our derived state. It uses an already existing state to dynamically derive the return value. Here’s how our render.php changes:

PHP
wp_interactivity_state( 'thm/counter', array(
    'counter' => 0,
    'double' => 0 * 2,
) );

<div
    data-wp-interactive="thm/counter"
    <?php echo get_block_wrapper_attributes(); ?>
>

    <button data-wp-on--click="actions.increment">Increment</button>
    <span data-wp-text="state.counter"></span>
    <span data-wp-text="state.double"></span>
</div>

And here is the final rendered HTML:

HTML
<div data-wp-interactive="thm/counter">
<button data-wp-on--click="actions.increment">Increment</button>
<span data-wp-text="state.counter">0</span>
<span data-wp-text="state.double">0</span>
</div>

You can see that we use state.double the exact same way as state.counter. As a matter of fact, there is absolutely no difference between these 2 types of state from the outside point of view. You should use derived state whenever the data depends solely on data from other state variables.

You can see we nonetheless initialize the ‘double’ in wp_interactivity_state(). That’s just the initial value, and you should usually define it – either statically or by computing it in PHP if you need to. How the value will later be computed on the frontend is defined solely in the state.double() function.

The API is smart. It knows that state.double is dependent on state.counter. Every time state.counter mutates, state.double will do too. That means our span displaying the double will automatically update every time we increment the counter.

Callbacks

Let’s add just one more thing to our block. We’ll make it log the counter’s value in the console every time it is incremented. For that, we’ll use a callback. Here’s our updated view.js (shortened):

JavaScript
import { store } from "@wordpress/interactivity";

const { state } = store( 'thm/counter', {
    // [...] state and actions
    callbacks: {
        logCounter: () => {
            console.log('Current counter: ' + state.counter);
        },
    },
} );

We then attach this callback with the wp-watch directive to the element with the wp-interactive directive. I will not insult your intelligence by showing you the entire render.php file again. Here is the rendered HTML:

HTML
<div data-wp-interactive="thm/counter" data-wp-watch="callbacks.logCounter">
<button data-wp-on--click="actions.increment">Increment</button>
<span data-wp-text="state.counter">0</span>
<span data-wp-text="state.double">0</span>
</div>

That’s it. The callback will be executed when the element is first created, and then each time state.counter changes. Again, the API is smart. The callback will only be called when the state it depends on mutates (the state used inside it, in our case it’s only state.conter). You can use callbacks with local context as well.

Server Side Rendering

You might be wondering “Why?”. Why create a proprietary JavaScript framework when there are so many available options? The answer lies in the requirements of this API. WordPress is still deeply rooted in PHP. Hooks need to have a way of modifying the output for plugins to be possible. None of the existing frameworks are PHP-friendly or compatible with WordPress.

There’s a reason why the “final rendered HTML” snippets in the examples above already contained the “0” in the span elements. The 0 is already there when the HTML is sent to the browser. That’s right – the directives are parsed and rendered on the backend using PHP. The API starts its job on the server, and continues working on the frontend. Other frameworks usually require a JavaScript-based server, such as NodeJS, to achieve the same thing.

But the WordPress team is smart. They didn’t reinvent the wheel. At the very foundation of this API sits Preact – a fast, minimalist react alternative. Overall, the API runs on about ~10 KB of JavaScript. And that JavaScript is shared across all blocks. If all block authors switched to using the Interactivity API instead of enqueuing react, svelte, or other libraries – that 10 KB is all we would ever need to easily make our blocks interactive.

PS: This API is not limited to blocks. It’s just JavaScript code and HTML data attributes. You absolutely can use it in non-block HTML, but it was written with blocks in mind, since blocks are the future of WordPress.