T-CREATOR

WordPress 技術で読み解く Interactivity API:動的 UI をノー JS フレームワークで実現

WordPress 技術で読み解く Interactivity API:動的 UI をノー JS フレームワークで実現

WordPress でインタラクティブな UI を作りたいとき、これまでは React や Vue.js といった JavaScript フレームワークを導入する必要がありました。しかし WordPress 6.5 から登場した Interactivity API を使えば、外部フレームワークなしで動的な UI を実現できます。

この記事では、Interactivity API の仕組みと実装方法を深掘りし、WordPress の新しい可能性を探っていきましょう。

背景

WordPress のブロックエディタ進化

WordPress は 5.0 で Gutenberg ブロックエディタを導入して以来、フロントエンド技術の近代化を進めてきました。ブロックエディタ自体は React で構築されていますが、これまでフロントエンド側でインタラクティブな機能を実装するには、開発者自身で JavaScript を組む必要がありました。

WordPress コアチームは、ブロック開発をより簡単にし、統一されたアプローチでインタラクティブ機能を提供する方法を模索していました。

従来の WordPress における動的 UI 実装

これまで WordPress で動的な UI を実装する方法は、主に以下の 3 つでした。

#方法メリットデメリット
1jQuery による DOM 操作学習コストが低い大規模になると保守が困難
2React/Vue.js の導入本格的な SPA が可能バンドルサイズ増大、WordPress との統合が複雑
3Vanilla JS軽量ステート管理が煩雑、コード量が多い

どの方法も一長一短があり、WordPress エコシステムとの親和性に課題がありました。

以下の図は、従来の WordPress における動的 UI 実装の選択肢を示しています。

mermaidflowchart TB
  req["動的 UI の要件"] --> choice{"実装方法<br/>の選択"}
  choice -->|シンプル| jquery["jQuery"]
  choice -->|本格的| framework["React/Vue.js"]
  choice -->|軽量志向| vanilla["Vanilla JS"]

  jquery --> problem1["保守性の課題"]
  framework --> problem2["バンドルサイズ<br/>増大"]
  vanilla --> problem3["実装コスト<br/>増大"]

  problem1 --> need["統一された<br/>アプローチが必要"]
  problem2 --> need
  problem3 --> need

上図のように、どの選択肢にも課題があり、WordPress コミュニティは統一されたソリューションを求めていたのです。

課題

JavaScript フレームワークの重複問題

WordPress サイトに複数のプラグインやテーマが導入されると、それぞれが異なる JavaScript フレームワークを使用している場合があります。React を使うプラグイン A、Vue.js を使うプラグイン B が同時に読み込まれると、ページの読み込み速度が著しく低下してしまうでしょう。

パフォーマンスの観点から、これは大きな問題でした。

ステート管理の複雑さ

シンプルなトグルボタンやアコーディオンメニューを実装するだけでも、従来は以下のような手順が必要でした。

  1. JavaScript でイベントリスナーを設定
  2. DOM 要素を取得してステートを管理
  3. ステート変更時に DOM を更新
  4. 複数コンポーネント間でステートを同期

この一連の流れを、開発者それぞれが独自の方法で実装していたため、コードの再利用性が低く、学習コストも高くなっていたのです。

下図は、従来のステート管理における課題を示しています。

mermaidsequenceDiagram
  participant User as ユーザー
  participant DOM as DOM 要素
  participant JS as JavaScript<br/>コード
  participant State as ステート管理

  User->>DOM: クリック
  DOM->>JS: イベント発火
  JS->>State: ステート取得
  State-->>JS: 現在のステート
  JS->>JS: 新しいステート計算
  JS->>State: ステート更新
  JS->>DOM: DOM 更新
  DOM-->>User: UI 変更表示

  Note over JS,State: 各開発者が独自実装<br/>統一性がない

WordPress エコシステムとの統合

WordPress のブロックエディタは React で作られていますが、フロントエンド側で同じ React を使おうとすると、バージョンの競合やバンドルの重複が発生する可能性があります。

また、PHP で生成される HTML と JavaScript のステートを同期させる仕組みも必要でした。サーバーサイドレンダリング(SSR)との親和性が課題だったのです。

解決策

Interactivity API の概要

WordPress 6.5 で正式に導入された Interactivity API は、これらの課題を解決するために設計されました。この API の最大の特徴は、HTML の属性ベースで動的な振る舞いを定義できる点にあります。

Interactivity API は以下の要素で構成されています。

#要素役割
1ディレクティブHTML 属性として記述する動作定義
2ストアグローバルなステート管理
3コンテキストコンポーネント間のデータ共有
4ランタイム軽量な JavaScript 実行環境

これらが組み合わさることで、React や Vue.js のようなフレームワークなしで、宣言的な UI を実現できるのです。

以下の図は、Interactivity API のアーキテクチャを示しています。

mermaidflowchart TB
  html["HTML with<br/>Directives"] --> runtime["Interactivity API<br/>Runtime"]
  store["Store<br/>(ステート管理)"] --> runtime
  context["Context<br/>(データ共有)"] --> runtime

  runtime --> dom["DOM 更新"]
  runtime --> events["イベント<br/>ハンドリング"]
  runtime --> state["ステート<br/>同期"]

  dom --> ui["動的 UI"]
  events --> ui
  state --> ui

  style runtime fill:#e1f5ff

ディレクティブの仕組み

Interactivity API では、data-wp-* という形式の属性(ディレクティブ)を HTML に追加することで、動的な振る舞いを定義します。

主要なディレクティブには以下のようなものがあります。

#ディレクティブ用途
1data-wp-interactiveインタラクティブ領域の定義ブロック全体のスコープ設定
2data-wp-bind属性のバインディングclass や aria 属性の動的変更
3data-wp-onイベントハンドラの登録クリックやフォーカスの処理
4data-wp-textテキストコンテンツの更新動的なテキスト表示
5data-wp-contextコンテキストデータの定義コンポーネント固有のステート

これらのディレクティブを組み合わせることで、複雑なインタラクションも実現できます。

ストアとコンテキスト

Interactivity API のステート管理は、ストア(Store)コンテキスト(Context) の 2 層構造になっています。

ストアはグローバルなステートとアクションを管理します。JavaScript ファイルで wp.interactivity() 関数を使って定義するのです。

コンテキストは、個々の HTML 要素に紐づくローカルなデータを管理します。data-wp-context 属性に JSON を指定することで、そのスコープ内でデータを共有できるでしょう。

この 2 層構造により、グローバルなロジックとコンポーネント固有のデータを適切に分離できます。

mermaidflowchart LR
  subgraph global["グローバル領域"]
    store["Store<br/>(共通ステート)"]
  end

  subgraph component1["コンポーネント A"]
    context1["Context A<br/>(ローカルデータ)"]
  end

  subgraph component2["コンポーネント B"]
    context2["Context B<br/>(ローカルデータ)"]
  end

  store --> context1
  store --> context2
  context1 -.->|独立| context2

  style store fill:#ffe1e1
  style context1 fill:#e1ffe1
  style context2 fill:#e1ffe1

WordPress コアとの統合

Interactivity API は WordPress コアに組み込まれているため、追加のライブラリをインストールする必要がありません。wp_enqueue_script() で専用のスクリプトを読み込むだけで利用できます。

また、ブロックエディタで使用される React との共存も考慮されており、バージョン競合の心配もないでしょう。

具体例

基本的なトグルボタンの実装

まずは、クリックすると表示/非表示が切り替わるシンプルなトグルボタンを実装してみます。

PHP 側の実装(render.php)

ブロックの render.php では、ディレクティブを含む HTML を出力します。

php<?php
/**
 * トグルボタンのレンダリング
 */
?>
<div
  <?php echo get_block_wrapper_attributes(); ?>
  data-wp-interactive="myPlugin/toggle"
  data-wp-context='{ "isOpen": false }'
>
  <!-- トグルボタン -->
  <button
    data-wp-on--click="actions.toggle"
    data-wp-bind--aria-expanded="context.isOpen"
  >
    メニューを開く
  </button>

  <!-- 表示/非表示されるコンテンツ -->
  <div
    data-wp-bind--hidden="!context.isOpen"
    data-wp-class--is-open="context.isOpen"
  >
    <p>これは表示されるコンテンツです。</p>
  </div>
</div>

上記のコードでは、以下のポイントに注目してください。

  • data-wp-interactive でインタラクティブな領域を宣言
  • data-wp-context で初期ステート(isOpen: false)を定義
  • data-wp-on--click でクリックイベントハンドラを登録
  • data-wp-bind--hidden で hidden 属性を動的に制御

JavaScript 側の実装(view.js)

次に、JavaScript でストアとアクションを定義します。

javascript/**
 * インポート文
 */
import { store } from '@wordpress/interactivity';
javascript/**
 * ストアの定義
 */
store('myPlugin/toggle', {
  /**
   * アクションの定義
   */
  actions: {
    /**
     * トグルアクション
     * コンテキストの isOpen を反転させる
     */
    toggle: ({ context }) => {
      context.isOpen = !context.isOpen;
    },
  },
});

たったこれだけで、インタラクティブなトグルボタンが完成します。従来の jQuery や Vanilla JS と比べて、コード量が大幅に削減されましたね。

カウンターコンポーネントの実装

次に、もう少し複雑な例として、カウンターコンポーネントを実装してみましょう。

PHP 側の実装(render.php)

php<?php
/**
 * カウンターコンポーネントのレンダリング
 */
?>
<div
  <?php echo get_block_wrapper_attributes(); ?>
  data-wp-interactive="myPlugin/counter"
  data-wp-context='{ "count": 0, "step": 1 }'
>
  <!-- カウント表示 -->
  <p>
    現在のカウント:
    <strong data-wp-text="context.count"></strong>
  </p>

  <!-- 操作ボタン -->
  <div class="counter-controls">
    <button data-wp-on--click="actions.decrement">
      - 減らす
    </button>

    <button data-wp-on--click="actions.increment">
      + 増やす
    </button>

    <button data-wp-on--click="actions.reset">
      リセット
    </button>
  </div>

  <!-- ステップ設定 -->
  <div class="counter-settings">
    <label>
      ステップ:
      <input
        type="number"
        value="1"
        data-wp-on--input="actions.setStep"
      />
    </label>
  </div>
</div>

このコードでは、data-wp-text を使ってカウント値を動的に表示しています。

JavaScript 側の実装(view.js)

javascript/**
 * カウンターストアの定義
 */
store('myPlugin/counter', {
  actions: {
    /**
     * カウントを増やす
     */
    increment: ({ context }) => {
      context.count += context.step;
    },

    /**
     * カウントを減らす
     */
    decrement: ({ context }) => {
      context.count -= context.step;
    },

    /**
     * カウントをリセット
     */
    reset: ({ context }) => {
      context.count = 0;
    },

    /**
     * ステップ値を設定
     * @param {Event} event - input イベント
     */
    setStep: ({ context, event }) => {
      const value = parseInt(event.target.value, 10);
      context.step = value || 1;
    },
  },
});

アクション内では、context を通じてコンポーネントのステートにアクセスできます。event パラメータを受け取ることで、イベントオブジェクトも利用可能です。

算出プロパティとウォッチャー

Interactivity API では、算出プロパティ(Computed)やウォッチャー(Callbacks)も利用できます。

算出プロパティの実装

javascript/**
 * 算出プロパティを持つストアの定義
 */
store('myPlugin/calculator', {
  state: {
    /**
     * 現在の価格
     */
    price: 1000,

    /**
     * 数量
     */
    quantity: 1,

    /**
     * 税率(10%)
     */
    get taxRate() {
      return 0.1;
    },

    /**
     * 合計金額(税込)を計算
     */
    get total() {
      const subtotal = this.price * this.quantity;
      return Math.floor(subtotal * (1 + this.taxRate));
    },
  },
});

算出プロパティは、依存する値が変更されると自動的に再計算されるため、手動で更新処理を書く必要がありません。

コールバックの実装

javascript/**
 * コールバックを持つストアの定義
 */
store('myPlugin/logger', {
  state: {
    count: 0,
  },

  actions: {
    increment: ({ state }) => {
      state.count++;
    },
  },

  /**
   * コールバック(副作用の処理)
   */
  callbacks: {
    /**
     * count が変更されたときにログ出力
     */
    logCount: ({ state }) => {
      console.log(`Count changed to: ${state.count}`);

      // 10 の倍数のときにアラート
      if (state.count % 10 === 0) {
        alert(`${state.count} に到達しました!`);
      }
    },
  },
});

callbacks は、ステートの変更を監視して副作用を実行する仕組みです。ロギングや外部 API の呼び出しなどに活用できるでしょう。

非同期処理の実装

実際のアプリケーションでは、API からデータを取得するような非同期処理が必要になります。

非同期アクションの実装

javascript/**
 * 非同期処理を含むストアの定義
 */
store('myPlugin/posts', {
  state: {
    /**
     * 投稿一覧
     */
    posts: [],

    /**
     * ローディング状態
     */
    isLoading: false,

    /**
     * エラーメッセージ
     */
    error: null,
  },

  actions: {
    /**
     * 投稿を取得する非同期アクション
     */
    fetchPosts: async ({ state }) => {
      state.isLoading = true;
      state.error = null;

      try {
        const response = await fetch(
          '/wp-json/wp/v2/posts'
        );

        if (!response.ok) {
          throw new Error('Failed to fetch posts');
        }

        const data = await response.json();
        state.posts = data;
      } catch (error) {
        state.error = error.message;
        console.error('Error fetching posts:', error);
      } finally {
        state.isLoading = false;
      }
    },
  },
});

非同期アクションでは、async​/​await 構文をそのまま使用できます。ローディング状態やエラーハンドリングも、通常の JavaScript と同じように実装可能です。

PHP 側での非同期対応

php<?php
/**
 * 非同期データ取得に対応した HTML
 */
?>
<div
  data-wp-interactive="myPlugin/posts"
  data-wp-context='{ "initialized": false }'
  data-wp-init="callbacks.init"
>
  <!-- ローディング表示 -->
  <div data-wp-bind--hidden="!state.isLoading">
    <p>読み込み中...</p>
  </div>

  <!-- エラー表示 -->
  <div
    data-wp-bind--hidden="!state.error"
    data-wp-class--error="state.error"
  >
    <p data-wp-text="state.error"></p>
  </div>

  <!-- 投稿一覧 -->
  <ul data-wp-bind--hidden="state.isLoading">
    <template data-wp-each="state.posts">
      <li data-wp-text="context.item.title.rendered"></li>
    </template>
  </ul>

  <!-- 再読み込みボタン -->
  <button data-wp-on--click="actions.fetchPosts">
    再読み込み
  </button>
</div>

data-wp-init ディレクティブを使うと、コンポーネントの初期化時に処理を実行できます。data-wp-each は配列をループして要素を生成するディレクティブです。

ブロック間の通信

複数のブロックが連携する場合、グローバルストアを活用します。

グローバルストアの定義

javascript/**
 * グローバルストアの定義
 */
store('myPlugin/global', {
  state: {
    /**
     * 選択されているタブ ID
     */
    selectedTab: 'tab1',

    /**
     * 通知メッセージ
     */
    notification: null,
  },

  actions: {
    /**
     * タブを選択
     */
    selectTab: ({ state }, tabId) => {
      state.selectedTab = tabId;
    },

    /**
     * 通知を表示
     */
    showNotification: ({ state }, message) => {
      state.notification = message;

      // 3 秒後に自動的に消す
      setTimeout(() => {
        state.notification = null;
      }, 3000);
    },
  },
});

異なるブロックからの参照

php<!-- ブロック A: タブナビゲーション -->
<div data-wp-interactive="myPlugin/global">
  <button
    data-wp-on--click="actions.selectTab"
    data-wp-on--click--tab="tab1"
  >
    タブ 1
  </button>
  <button
    data-wp-on--click="actions.selectTab"
    data-wp-on--click--tab="tab2"
  >
    タブ 2
  </button>
</div>

<!-- ブロック B: タブコンテンツ -->
<div data-wp-interactive="myPlugin/global">
  <div data-wp-bind--hidden="state.selectedTab !== 'tab1'">
    <p>タブ 1 のコンテンツ</p>
  </div>
  <div data-wp-bind--hidden="state.selectedTab !== 'tab2'">
    <p>タブ 2 のコンテンツ</p>
  </div>
</div>

グローバルストアを共有することで、異なるブロック間でもステートを同期できますね。

パフォーマンス最適化

Interactivity API は軽量ですが、さらなる最適化も可能です。

条件付きスクリプト読み込み

php<?php
/**
 * ブロックが実際に使用されている場合のみスクリプトを読み込む
 */
function my_plugin_enqueue_scripts() {
  // ブロックのスクリプトを登録
  wp_register_script(
    'my-plugin-view',
    plugins_url('build/view.js', __FILE__),
    array('wp-interactivity'),
    filemtime(plugin_dir_path(__FILE__) . 'build/view.js'),
    true
  );
}
add_action('init', 'my_plugin_enqueue_scripts');
php<?php
/**
 * render.php でスクリプトを enqueue
 */
wp_enqueue_script('my-plugin-view');
?>
<div data-wp-interactive="myPlugin/example">
  <!-- コンテンツ -->
</div>

このように、ブロックが実際にレンダリングされるときだけスクリプトを読み込むことで、無駄なリソース消費を防げます。

メモ化とデバウンス

javascript/**
 * パフォーマンス最適化を含むストア
 */
store('myPlugin/search', {
  state: {
    query: '',
    results: [],
  },

  actions: {
    /**
     * 検索クエリを更新(デバウンス付き)
     */
    updateQuery: ({ state, event }) => {
      const value = event.target.value;
      state.query = value;

      // デバウンス処理
      if (state.debounceTimer) {
        clearTimeout(state.debounceTimer);
      }

      state.debounceTimer = setTimeout(() => {
        state.performSearch();
      }, 300);
    },

    /**
     * 実際の検索を実行
     */
    async performSearch({ state }) {
      if (!state.query) {
        state.results = [];
        return;
      }

      const response = await fetch(
        `/wp-json/wp/v2/search?search=${state.query}`
      );
      state.results = await response.json();
    },
  },
});

デバウンスを使うことで、ユーザーが入力中に無駄な API リクエストを送らずに済みます。

まとめ

WordPress Interactivity API は、動的な UI を構築するための新しいアプローチを提供してくれます。React や Vue.js といった外部フレームワークを導入することなく、HTML の属性ベースで宣言的にインタラクティブな機能を実装できるのです。

この API の主な利点をまとめましょう。

#利点詳細
1軽量性外部フレームワーク不要でバンドルサイズが小さい
2統一性WordPress エコシステム全体で一貫したアプローチ
3学習コストHTML 属性ベースで直感的に理解しやすい
4SSR 親和性PHP との統合が容易でサーバーサイドレンダリングと相性が良い
5保守性宣言的な記述でコードの見通しが良い

Interactivity API はまだ新しい技術ですが、WordPress の今後の方向性を示す重要な機能と言えるでしょう。ブロックテーマの普及とともに、この API を活用した動的なブロックがますます増えていくことが期待されます。

あなたも Interactivity API を使って、より豊かなユーザー体験を提供する WordPress サイトを構築してみてはいかがでしょうか。

関連リンク