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 つでした。
| # | 方法 | メリット | デメリット |
|---|---|---|---|
| 1 | jQuery による DOM 操作 | 学習コストが低い | 大規模になると保守が困難 |
| 2 | React/Vue.js の導入 | 本格的な SPA が可能 | バンドルサイズ増大、WordPress との統合が複雑 |
| 3 | Vanilla 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 が同時に読み込まれると、ページの読み込み速度が著しく低下してしまうでしょう。
パフォーマンスの観点から、これは大きな問題でした。
ステート管理の複雑さ
シンプルなトグルボタンやアコーディオンメニューを実装するだけでも、従来は以下のような手順が必要でした。
- JavaScript でイベントリスナーを設定
- DOM 要素を取得してステートを管理
- ステート変更時に DOM を更新
- 複数コンポーネント間でステートを同期
この一連の流れを、開発者それぞれが独自の方法で実装していたため、コードの再利用性が低く、学習コストも高くなっていたのです。
下図は、従来のステート管理における課題を示しています。
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 に追加することで、動的な振る舞いを定義します。
主要なディレクティブには以下のようなものがあります。
| # | ディレクティブ | 用途 | 例 |
|---|---|---|---|
| 1 | data-wp-interactive | インタラクティブ領域の定義 | ブロック全体のスコープ設定 |
| 2 | data-wp-bind | 属性のバインディング | class や aria 属性の動的変更 |
| 3 | data-wp-on | イベントハンドラの登録 | クリックやフォーカスの処理 |
| 4 | data-wp-text | テキストコンテンツの更新 | 動的なテキスト表示 |
| 5 | data-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 属性ベースで直感的に理解しやすい |
| 4 | SSR 親和性 | PHP との統合が容易でサーバーサイドレンダリングと相性が良い |
| 5 | 保守性 | 宣言的な記述でコードの見通しが良い |
Interactivity API はまだ新しい技術ですが、WordPress の今後の方向性を示す重要な機能と言えるでしょう。ブロックテーマの普及とともに、この API を活用した動的なブロックがますます増えていくことが期待されます。
あなたも Interactivity API を使って、より豊かなユーザー体験を提供する WordPress サイトを構築してみてはいかがでしょうか。
関連リンク
articleWordPress 技術で読み解く Interactivity API:動的 UI をノー JS フレームワークで実現
articleバックアップ戦略の決定版:WordPress の世代管理/災害復旧の型
articleプラグイン競合の特定術:WordPress で原因切り分けを高速化する手順
article画像最適化比較:WordPress の WebP/AVIF/外部 CDN を実測レビュー
articleWordPress URL 設計とリライトルール:正規化と SEO を両立する作法
articleWordPress × Bedrock/Composer 入門:プラグイン管理をコード化する
articlemacOS(Apple Silicon)で Docker を高速化:qemu/仮想化設定・Rosetta 併用術
articleCline × クリーンアーキテクチャ:ユースケース駆動と境界の切り出し
articleDevin 用リポジトリ準備チェックリスト:ブランチ戦略・CI 前提・テスト整備
articleClaude Code プロンプト設計チートシート:役割・入力・出力フォーマット定番集
articleConvex と Next.js Server Actions “直書き”比較:保守性・安全性・速度をコードで実測
articleBun でリアルタイムダッシュボード:メトリクス集計と可視化を高速化
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来