Unleash the power of the Interactivity API (IAPI): A New Era for WordPress

colorfultones.com/presentation/wcus-2024/

Fasten your seatbelts! 🏎️

  • What is the Interactivity API?
  • Why might I need it and how does it work?
  • When should I use it?
  • Where is the roadmap headed?

What do we mean by interactivity?

Query Loop block – seamless pagination

Image block – Lightbox

WPMovies.dev

Pew Research Center – data tables with filtering

Pew Research Center – facet filtering for query block

What do we mean by interactivity?

  • Composable blocks
  • Shared state

Why do we need an Interactivity API in WordPress? 🤔

The goals of the Interactivity API

Block-first and PHP-first

Backward compatible

Optional and gradual adoption

Declarative and reactive

Performant

Extensible

Atomic and composable

Compatible with existing block development tooling

Allow for client-side navigation – In Progress: see #60951

How does it work?

Its three main components are:

  • Server-side logic, handled by the HTML_Tag_Processor.
  • Preact combined with Preact Signals for hydration, client logic, and client-side navigation.
  • HTML Directives that can be understood by both the client and server.

Core concepts

  • The Reactive and Declarative mindset
  • Understanding global state, local context and derived state
  • Server-side rendering: Processing directives on the server

Reactive + Declarative = IAPI

Declarative vs. Imperative

  • Imperative – how to accomplish tasks by explicitly stating each step to manipulate the program’s state.
  • Declarative – what a program should accomplish.

The Imperative approach

HTML

🔴 🟡 🟢

some-plugin.html

<div id="my-interactive-plugin">
    <button
        id="show-hide-btn"
        aria-expanded="false"
        aria-controls="status-paragraph"
    >
        show
    </button>
    <button id="activate-btn" disabled>activate</button>
    <p id="status-paragraph" class="inactive" hidden>this is inactive</p>
</div>
JavaScript

🔴 🟡 🟢

some-plugin.js

const showHideBtn = document.getElementById( 'show-hide-btn' );
const activateBtn = document.getElementById( 'activate-btn' );
const statusParagraph = document.getElementById( 'status-paragraph' );

showHideBtn.addEventListener( 'click', () => {
    if ( statusParagraph.hasAttribute( 'hidden' ) ) {
        statusParagraph.removeAttribute( 'hidden' );
        showHideBtn.textContent = 'hide';
        showHideBtn.setAttribute( 'aria-expanded', 'true' );
        activateBtn.removeAttribute( 'disabled' );
    } else {
        if ( statusParagraph.classList.contains( 'active' ) ) {
            statusParagraph.textContent = 'this is inactive';
            statusParagraph.classList.remove( 'active' );
            activateBtn.textContent = 'activate';
        }
        statusParagraph.setAttribute( 'hidden', true );
        showHideBtn.textContent = 'show';
        showHideBtn.setAttribute( 'aria-expanded', 'false' );
        activateBtn.setAttribute( 'disabled', true );
    }
} );

activateBtn.addEventListener( 'click', () => {
    if ( activateBtn.textContent === 'activate' ) {
        statusParagraph.textContent = 'this is active';
        statusParagraph.classList.remove( 'inactive' );
        statusParagraph.classList.add( 'active' );
        activateBtn.textContent = 'deactivate';
    } else {
        statusParagraph.textContent = 'this is inactive';
        statusParagraph.classList.remove( 'active' );
        statusParagraph.classList.add( 'inactive' );
        activateBtn.textContent = 'activate';
    }
} );

The Declarative approach

HTML

🔴 🟡 🟢

render.php

<div id="my-interactive-plugin" data-wp-interactive="myInteractivePlugin">
    <button
        data-wp-on--click="actions.toggleVisibility"
        data-wp-bind--aria-expanded="state.isVisible"
        data-wp-text="state.visibilityText"
        aria-controls="status-paragraph"
    >
        show
    </button>
    <button
        data-wp-on--click="actions.toggleActivation"
        data-wp-bind--disabled="!state.isVisible"
        data-wp-text="state.activationText"
    >
        activate
    </button>
    <p
        id="status-paragraph"
        data-wp-bind--hidden="!state.isVisible"
        data-wp-class--active="state.isActive"
        data-wp-class--inactive="!state.isActive"
        data-wp-text="state.paragraphText"
    >
        this is inactive
    </p>
</div>
JavaScript

🔴 🟡 🟢

view.js

import { store } from '@wordpress/interactivity';

const { state } = store( 'myInteractivePlugin', {
    state: {
        isVisible: false,
        isActive: false,
        get visibilityText() {
            return state.isVisible ? 'hide' : 'show';
        },
        get activationText() {
            return state.isActive ? 'deactivate' : 'activate';
        },
        get paragraphText() {
            return state.isActive ? 'this is active' : 'this is inactive';
        },
    },
    actions: {
        toggleVisibility() {
            state.isVisible = ! state.isVisible;
            if ( ! state.isVisible ) state.isActive = false;
        },
        toggleActivation() {
            state.isActive = ! state.isActive;
        },
    },
} );

Reactivity

IAPI uses a fine-grained reactivity system:

  1. Reactive state: Mutates based on global and local state and updates the UI. There are three types of state: global, local, and derived.
  2. Actions: These are functions, usually triggered by event handlers, that mutate the global state or local context.
  3. Reactive Bindings: HTML elements are bound to reactive state values using special attributes like data-wp-binddata-wp-text, or data-wp-class.
  4. Automatic Updates: When the actions mutate the global state or local context, the Interactivity API automatically updates all the parts of the DOM that depend on that state (either directly or through the derived state).

Types of reactive state

  1. Global state – can be shared between blocks
  2. Local context – specific to the targeted element and its children
  3. Derived state – computed properties that update when their dependencies change

Breaking it down

🔴 🟡 🟢

view.js

const { state } = store( 'myInteractivePlugin', {
    state: {
        isVisible: false, // state properties
        isActive: false,
        // Derived state, automatically updates
        get visibilityText() {
            return state.isVisible ? 'hide' : 'show';
        },
        // ... other derived state
    },
    actions: {
        // Modifies the state
        toggleVisibility() {
            state.isVisible = ! state.isVisible;
        },
        // ... other actions
    },
} );

Server Directive Processing

Global state

🔴 🟡 🟢

render.php

<?php
// Defining global state.
wp_interactivity_state( 'myFruitPlugin', array(
    'fruits' => array( 'Apple', 'Banana', 'Cherry' )
) );
?>

<ul data-wp-interactive="myFruitPlugin">
    <template data-wp-each="state.fruits">
        <li data-wp-text="context.item"></li>
    </template>
</ul>
Local context (inline)

🔴 🟡 🟢

render.php

<ul data-wp-context='{ "fruits": ["Apple", "Banana", "Cherry"] }'>
    ...
</ul>
Local context (wp_interactivity_data_wp_context)

🔴 🟡 🟢

render.php

<?php
    $context = array( 'fruits' => array( 'Apple', 'Banana', 'Cherry' ) );
?>

<ul <?php echo wp_interactivity_data_wp_context( $context ); ?>>
  ...
</ul>
Resulting HTML markup

🔴 🟡 🟢

<ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Cherry</li>
</ul>

Directives

Custom attributes are added to your block’s markup to add behavior to its DOM elements. IAPI directives use the data-wp—prefix.

🔴 🟡 🟢

render.php

<div
  data-wp-interactive="myPlugin"
  data-wp-context='{ "isOpen": false }'
  data-wp-watch="callbacks.logIsOpen"
>
  <button
    data-wp-on--click="actions.toggle"
    data-wp-bind--aria-expanded="context.isOpen"
    aria-controls="p-1"
  >
    Toggle
  </button>
  <p id="p-1" data-wp-bind--hidden="!context.isOpen">
    This element is now visible!
  </p>
</div>

wp-interactive directive

“Activates” the interactivity for the DOM element and its children.

Simple string example

🔴 🟡 🟢

render.php

<!-- Let's make this element and its children interactive and set the namespace -->
<div
  data-wp-interactive="myPlugin"
  data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
  <p>I'm interactive now, <span data-wp-style--background-color="context.myBgColor">and I can use directives!</span></p>
  <div>
    <p>I'm also interactive, <span data-wp-style--color="context.myColor">and I can also use directives!</span></p>
  </div>
</div>
Namespace object example

🔴 🟡 🟢

render.php

<!-- Let's make this element and its children interactive and set the namespace -->
<div
  data-wp-interactive='{ "namespace": "myPlugin" }'
  data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
  <p>I'm interactive now, <span data-wp-style--background-color="context.myBgColor">and I can use directives!</span></p>
  <div>
    <p>I'm also interactive, <span data-wp-style--color="context.myColor">and I can also use directives!</span></p>
  </div>
</div>

wp-context

Provides a local state available to a specific HTML node and its children.

Context directive in render.php

🔴 🟡 🟢

render.php

<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }'>
  <button data-wp-on--click="actions.logId">
    Click Me!
  </button>
</div>
Referencing the store

🔴 🟡 🟢

view.js

store( "myPlugin", {
  actions: {
    logId: () => {
      const { post } = getContext();
      console.log( post.id );
    },
  },
} );

Nesting wp-context

Different contexts can be defined at different levels, and deeper levels will merge their context with any parent.

🔴 🟡 🟢

render.php

<div data-wp-context="{ foo: 'bar' }">
  <span data-wp-text="context.foo"><!-- Will output: "bar" --></span>

  <div data-wp-context="{ bar: 'baz' }">
    <span data-wp-text="context.foo"><!-- Will output: "bar" --></span>

    <div data-wp-context="{ foo: 'bob' }">
      <span data-wp-text="context.foo"><!-- Will output: "bob" --></span>
    </div>

  </div>
</div>

The store

Used to create the logic (actions, side effects, etc.) linked to the directives and the data used inside that logic (state, derived state, etc.).

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

Create a new block with IAPI

🔴 🟡 🟢

npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template

🔴 🟡 🟢

cd my-first-interactive-block && npm start

Integrate IAPI into an existing block

Add interactivity support in block.json

🔴 🟡 🟢

block.json

"supports": {
    "interactivity": true
},
"viewScriptModule": "file:./view.js"

Add wp-interactive directive to a DOM element

🔴 🟡 🟢

render.php

<div data-wp-interactive="myPlugin">
    <!-- Interactivity API zone -->
</div>

Existing examples

Key pieces of IAPI

  • Directives (HTML attributes: data-wp-xyz)
  • Store – logic and data manipulation (state, actions, side-effects)