Interactivity API in 6.5

The Interactivity APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. provides a standard way for developers to add interactions to the frontend of their blocks.

This standard aims to make it easier for developers to create rich, interactive user experiences, from simple cases like counters or pop-ups to more complex features like instant page navigation, instant search, carts, or checkouts.

Blocks can share data, actions, and callbacks between them. This makes communication between blocks simpler and less error-prone. For example, clicking on an “add to cart” blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. can seamlessly update a separate “cart” block.

To understand better the reasoning behind it, you can take a look at the original proposal, where it is explained in more detail.

More information about it can be found in the merge announcement, the status update post, and the Trac ticket for the Interactivity API.

This dev notedev note Each important change in WordPress Core is documented in a developers note, (usually called dev note). Good dev notes generally include a description of the change, the decision that led to this change, and a description of how developers are supposed to work with that change. Dev notes are published on Make/Core blog during the beta phase of WordPress release cycle. Publishing dev notes is particularly important when plugin/theme authors and WordPress developers need to be aware of those changes.In general, all dev notes are compiled into a Field Guide at the beginning of the release candidate phase. covers the APIs included in 6.5 and how to use the Interactivity API.

How to create interactions using the Interactivity API

It’s important to highlight that the block creation workflow doesn’t change.

Until now, WordPress has been intentionally unopinionated about the different solutions used on the frontend of blocks. The Interactivity API changes that. It adds a new standard way to easily add frontend interactivity to blocks while the APIs handling the Block Editor remain the same.

You need first to declare its compatibility with the API by adding the interactivity property inside  supports, in the block.json file:

"supports": {
    "interactivity": true
},

Refer to the Block Editor handbook to get a more detailed description of the interactivity support property.

The Interactivity API script requires using the new script modules coming in WordPress 6.5, so blocks should enqueue the JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. by using viewScriptModule:

// block.json
{
   ...
   "viewScriptModule": "file:./view.js"
}

You can easily scaffold and test an interactive block following this quick start guide, which explains how to use a CLICLI Command Line Interface. Terminal (Bash) in Mac, Command Prompt in Windows, or WP-CLI for WordPress. command to speed up this process.

With that in mind, in order to add interactivity to blocks powered by the Interactivity API, developers would need to:

  1. Add directives to the markup to add specific interactions to the block.
  2. Create a store with the logic (state, actions, or callbacks) for interactivity.

Let’s use a simple example to explain it: a button that shows and hides some text. Let’s also send a message in the console whenever the button is hidden or revealed.

1. Add the directives

Directives are custom attributes that are added to the markup of your block to add interactions to its DOM elements. They are placed in the render.php file (for dynamic blocks).

The very first step is to add the data-wp-interactive directive. This is used to “activate” the Interactivity API in a DOM element and its children, and its value must be the unique namespace of your pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party or block:

<pre class="wp-block-syntaxhighlighter-code"><div data-wp-interactive="myPlugin">
    <!-- Interactivity API zone -->
</div></pre>

The rest of the directives can be added with the desired interactions.

<pre class="wp-block-syntaxhighlighter-code">// render.php

$context = array('isOpen' => false);

<div
  
  
  data-wp-interactive='myPlugin'
  data-wp-watch="callbacks.logIsOpen"
>
  <button>
    Toggle
  </button>
  <p id="p-1">
    This element is now visible!
  </p>
</div></pre>

Additionally, directives can also be injected dynamically using the HTML Tag Processor.

Don’t worry if you don’t understand how it works yet. So far, the important part is that the example above uses directives like wp-on and wp-bind to add interactivity to the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers.. This is the list of directives available in WordPress 6.5:

You can find a deeper explanation of each directive and examples of how to use it in the relevant links.

  • wp-interactive: This attribute must be set to the unique identifier of your plugin or block in order for it to use the Interactivity API.
  • wp-context: It provides a local state available to a specific HTML node and its children. It accepts stringified JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. as a value. It’s recommended to use wp_interactivity_data_wp_context() to set it in PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher.
  • wp-bind: It allows HTML attributes to be set on elements based on a boolean or string value. It follows the syntax data-wp-bind--[attribute]. (like data-wp-bind--value)
  • wp-class: It adds or removes a class to an HTML element, depending on a boolean value. It follows the syntax data-wp-class--[classname].
  • wp-style: It adds or removes inline style to an HTML element, depending on its value. It follows the syntax data-wp-style--[css-property].
  • wp-text: It sets the inner text of an HTML element. It only accepts strings as the parameter.
  • wp-on: It runs code on dispatched DOM events like click or keyup. Its syntax is data-wp-on--[event] (like data-wp-on--click or data-wp-on--keyup).
  • wp-on-window: It allows to attach global window events like resize, copy, focus and then execute a defined callback when those happen. Its syntax is data-wp-on-window--[window-event] (like data-wp-on-window--resize or data-wp-on-window--languagechange).
  • wp-on-document: It allows to attach global document events like scroll, mousemove, keydown and then execute a defined callback when those happen. Its syntax is data-wp-on-document--[document-event] (like data-wp-on-document--keydown or data-wp-on-document--selectionchange).
  • wp-watch: It runs a callback when the node is created and runs it again when the state or context changes.
  • wp-init: It runs a callback only when the node is created.
  • wp-run: It runs the passed callback during node’s render execution.
  • wp-key: It assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements.
  • wp-each: It is intended to render a list of elements.
  • wp-each-child: Ensures hydration works as expected, is added automatically on the server processing of wp-each directive.

2. Create the store

The store is used to create the logic that will link the directives with the data used inside that logic.

All stores are referenced by a unique namespace, separating the logic and avoiding name collisions between different store properties and functions.

If there are multiple stores defined with the same namespace, they will be merged into a single store.

The store is usually created in the view.js file of each block, although the state can be initialized in the backend, for example, in the render file of the block.

The state is a global object, available to all HTML nodes of the page. It is defined by the store() function. If you need a local state for just a node and its children, check the context definition.

The object can accept any property, in order to keep consistency between projects, this convention is recommended.

  • State: Defines data available to the HTML nodes of the page. Properties inside the state will be available globally. If you need to edit them, the recommended way is by using getters.
    • Derived State. If you need a modified version of any state property, getters are the recommended approach (more on deriving state below).
  • Actions: Usually triggered by the data-wp-on directive (using event listeners).
  • Callbacks: Automatically reactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/. to state changes. Usually triggered by data-wp-on-window, data-wp-on-document or data-wp-init directives.

Returning to our example, this could be a simple store in one block, a global state has been added for having a complete sample of how a store could look like.

// view.js
import { store, getContext } from "@wordpress/interactivity";

const { state } = store( 'myPlugin', {
 state: {
  likes: 0,
  getDoubleLikes() {
    return 2 * state.likes;
  }
 },
  actions: {
    toggle: () => {
      const context = getContext();
      context.isOpen = !context.isOpen;
    },
  },
  callbacks: {
    logIsOpen: () => {
      const context = getContext();
      // Log the value of `isOpen` each time it changes.
      console.log(`Is open: ${context.isOpen}`);
    },
  },
});

There can be cases where only actions and callbacks are defined in the store.

DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see diagram).

When creating the store, there are some important things to be aware of:

Using derived state

Derived state uses getters to return a computed version of the state. It can access both state and context.

// view.js
const { state } = store( "myPlugin", {
  state: {
    amount: 34,
    defaultCurrency: 'EUR',
    currencyExchange: {
      USD: 1.1,
      GBP: 0.85,
    },
    get amountInUSD() {
      return state.currencyExchange[ 'USD' ] * state.amount,
    },
    get amountInGBP() {
      return state.currencyExchange[ 'GBP' ] * state.amount,
    },
  },
} );

Accessing the store by destructuring

The store contains all the store properties, like state, actions, or callbacks. They are returned by the store() call, so you can access them by destructuring it:

const { state, actions, callbacks } = store( "myPlugin", {
  // ...
} );

Note that context is not part of the store and is accessed through the getContext function.

If you want to take a deeper view about how the store() function works, feel free to check the function documentation here.

Async actions

Async actions should use generator functions instead of async/await or promises. The Interactivity API needs to be able to track async behavior in order to restore the proper scope. Otherwise, getContext may return stale values if it was updated concurrently with the async operation. Instead of awaiting the promise, yield it from the generator function, and the Interactivity API will handle awaiting its completion.

So, instead of:

store("myPlugin", {
  state: {
    get isOpen() {
      return getContext().isOpen;
    },
  },
  actions: {
    someAction: async () => {
      state.isOpen; // This is the expected context.
      await longDelay();
      state.isOpen; // This may not get the proper context unless it's properly restored.
    },
  },
});

function longDelay() {
  return new Promise( ( resolve ) => {
    setTimeout( () => resolve(), 3_000 );
  } );
}

The store should be:

store("myPlugin", {
  state: {
    get isOpen() {
      return getContext().isOpen;
    },
  },
  actions: {
    someAction: function* () {
      state.isOpen; // This is the expected context.
      yield longDelay(); // With generators, the caller controls when to resume this function.
      state.isOpen; // This context is correct because the scope was restored before resuming after the yield.
    },
  },
});

If you want to take a deeper look at the example, check the api reference.


Working with other namespaces

Interactive blocks can share data between them, unless they are private stores.

Directives

In order to access the store of a different namespace in a directive, add the namespace before the directive value. For example:

<pre class="wp-block-syntaxhighlighter-code"><!-- This accesses the current store -->
<div></div>

<!-- This accesses the "otherPlugin" store -->
<button>Button</button></pre>

Context

Context from a different namespace can be accessed by providing the desired namespace as an argument to getContext( namespace ):

import { getContext } from "@wordpress/interactivity";

const otherPluginContext = getContext( "otherPlugin" );

Store

Like context, different stores can be accessed by passing the desired namespace as an argument: 

const { state: otherState, actions: otherActions } = store( "otherPlugin" );

Private stores

A store can be “locked” to prevent its content from being accessed from other namespaces. To do so, set the lock option to true in the store() call, like in the example below. When the lock is set, subsequent executions of store() with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first store() call. This is especially useful for developers who want to hide part of their plugin stores so it doesn’t become accessible for extenders.

const { state } = store("myPlugin/private", {
  state: {
      messages: [ "private message" ]
    } 
  },
  { lock: true }
);

// The following call throws an Error!
store( "myPlugin/private", { /* store part */ } );

There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the lock value. Such a string can then be used in subsequent store() calls to the same namespace to unlock its content. Only the code with the lock string will be able to access the protected store. This is useful for complex stores defined across multiple files.

const { state } = store("myPlugin/private", {
  state: {
      messages: [ "private message" ]
    }
  },
  { lock: PRIVATE_LOCK }
);

// The following call works as expected.
store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } );

Interactivity API client methods

The following methods are for use in JavaScript and are provided by the wordpress/interactivity script module available in WordPress 6.5.

getContext()

The context defined with the data-wp-context attribute can be retrieved with the getContext function:

const { state } = store( "myPlugin", {
  actions: {
    someAction() {
      const context = getContext();
      const otherPluginContext = getContext( 'otherPlugin' );
      // ...
    }
  }
} );

Handbook description.

getElement()

Retrieves a representation of the element where a function from the store is being evaluated. This representation is read-only, and contains a reference to the DOM element and its attributes.

Handbook description.

getConfig()

Retrieves a configuration object that was previously defined in the server via wp_interactivity_config() function.

Configuration is immutable on the client, it cannot be modified. You can get an example later in this document.

store()

Creates the store used to link the data and actions with their respective directives. Check the main section for more information.

withScope()

Actions can depend on the scope when they are called, e.g., when you call getContext() or getElement().

When the Interactivity API runtime execute callbacks, the scope is set automatically. However, if you call an action from a callback that is not executed by the runtime, like in a setInterval() callback, you need to ensure that the scope is properly set. Use the withScope() function to ensure the scope is properly set in these cases.

An example, where actions.nextImage would trigger an undefined error without the wrapper:

store('mySliderPlugin', {
	callbacks: {
		initSlideShow: () => {
		    setInterval(
				withScope( () => {
					actions.nextImage();
				} ),
				3_000
			);
		}
	},
})

Interactivity API server functions

These are the PHP functions the Interactivity API includes:

wp_interactivity_state( $store_namespace, $state )

It is used to initialize the state on the server and ensure that the HTML sent by it, and the HTML after the client hydration are the same. And it also allows you to use any WordPress API like coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. translations.

// render.php

wp_interactivity_state( "movies", array(
      "1" => array(
        "id" => "123-abc",
        "movieName" => __("someMovieName", "textdomain")
      ),
) );

It receives two arguments, a string with the namespace that will be used as a reference and an associative array containing the values.

The state defined on this function gets merged with the stores defined in the view.js files.

wp_interactivity_data_wp_context( $context, $store_namespace )

Generates a data-wp-context attribute ready to be server side rendered. This function escapes the array to prevent external attacks, apart from any error that may appear when writing JSON strings manually.

$context is an array containing the keys and values of the context.

$store_namespace allows referencing different stores, and is empty by default.

<pre class="wp-block-syntaxhighlighter-code"> $post_id,
  'show' => true,
 );
?>
<div  >
  My interactive div
</div></pre>

Will return

<pre class="wp-block-syntaxhighlighter-code"><div>
  My interactive div
</div></pre>

wp_interactivity_config( $store_namespace, $config )

Sets or gets configuration for an interactivity store. An immutable copy of the configuration can be read by the client.

Consider config as a global setting that can affect the full site and won’t be updated on client interactions. For example, determining if a site can handle client-side navigation or not.

 true ) );

// Gets the current configuration for the 'myPlugin' namespace.
$config = wp_interactivity_config( 'myPlugin' );

This config can be retrieved in the client:

// view.js

const { setting } = getConfig();
console.log( setting ); // Will log true.

wp_interactivity_process_directives( $html )

Processes directives within HTML content, updating the markup where necessary.

This is the core functionality of the Interactivity API. It’s public so that any HTML can be processed, not just blocks.

For blocks with supports.interactivity, directives are automatically processed. Developers do not need to call wp_interactivity_process_directives in this case.

<pre class="wp-block-syntaxhighlighter-code"><?php
$html_content = '<div data-wp-text="myPlugin::state.message"></div>';
wp_interactivity_state( 'myPlugin', array( 'message' => 'hello world!' ) );

// Process directives in HTML content.
$processed_html = wp_interactivity_process_directives( $html_content );
// output: <div data-wp-text="myPlugin::state.message">hello world!</div></pre>

Relevant links

Props to @gziolo, @darerodz, @santosguillamot, @luisherranz and @jonsurrell for technical review.

Props to @leonnugraha for copy review.

#6-5, #dev-notes, #dev-notes-6-5, #interactivity-api