Interactivity API は React、Vue、Svelte、Alpine のような他のモダンなフレームワークと同様の、リアクティブで宣言的なフレームワークです。Interactivity API の操作において、その可能性を最大限に引き出すには、正しい考え方を採用することが非常に重要です。このガイドではリアクティブと宣言型のコアコンセプトを説明し、Interactivity API を効果的に使用するための基礎を提供します。
宣言型 vs. 実装型
宣言型プログラミングでは、プログラムが 何を 達成すべきかを記述します。そこでは希望する結果にフォーカスし、結果を達成するためのコマンドやステップを明示的に列挙しません。対照的に命令型プログラミングでは、どのように タスクを達成するかを指定します。そこでは明示的にプログラムの状態 (ステート) を操作する個々のステップを記述します。
命令型アプローチ
ウェブ開発の初期は命令型アプローチが主流でした。この方法では変更を反映するために、JavaScript を使用して手動で DOMを更新しました。
たとえば、以下は2つのボタンと段落からなるインタラクティブブロックです。
- show / hide (表示 / 非表示) ボタン: 段落の表示 / 非表示を切り替え、「activate (有効化)」ボタンの有効 / 無効を切り替えます。
- activate / deactivate (有効化 / 無効化) ボタン: 段落のテキストと色を「有効」(緑) と「無効」 (赤) の間で切り替えます。
<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>
<script>
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';
}
} );
</script>
見て分かるように、それぞれの条件に対する DOM 内のすべての変化を、以前の状態に応じて JavaScript で修正しなければなりません。
宣言型アプローチ
宣言型アプローチは、何が 起こるべきかに焦点を当てることでプロセスを単純化します。UI は状態の変化に応じて自動的に更新されます。以下は、Interactivity APIの宣言的アプローチを使用した同様の例です。
<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>
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;
},
},
} );
この宣言型の例では、UI は現在の状態 (ステート) に基づいて自動的に更新されます。開発者が行うことは、必要なステート、派生ステート、ステートを変更するアクション、DOM のどの部分がステートのどの部分に依存するかを宣言するだけです。フレームワークがすべての必要な DOM の更新を行うため、DOM は常に現在のステートと同期します。フレームワークによって制御される要素の数に関係なく、ロジックはシンプルで保守性が維持されます。
ところでバグに気がつきましたか ?
上の命令型アプローチの例には、教訓のため意図的にバグが仕込まれています。見つけられますか ? 簡単ではありませんよ ! (答えは末尾に)
この種のバグは命令型コードでは頻繁に発生します。これはすべての条件を手動で制御しなければならないためです。一方、宣言的なコードではこのようなバグは存在しません。フレームワークが DOM の更新を引き受け、かつ、何も見落とさないからです。
宣言型アプローチの利点
上の例で見たように命令型アプローチでは、詳細なステップと DOM の直接操作が必要で、インタラクティブ性が複雑になるとすぐに複雑になり、メンテナンスが難しくなります。取り得る状態 (ステート) や要素が増えれば増えるほど条件のロジックを追加する必要があり、コードは指数関数的に複雑になります。一方、宣言型アプローチではステートを管理し、DOM の更新をフレームワークに任せることでプロセスを単純化します。コードは読みやすく、保守しやすく、スケーラブルになります。
リアクティブ
Interactivity API はリアクティブを活用した宣言的なフレームワークです。リアクティブなシステムでは、データの変更は自動的にユーザーインターフェースの更新をトリガーし、ビューが常にアプリケーションの現在のステートを反映することが保証されます。
リアクティブが動作する仕組み
Interactivity API は、きめ細かなリアクティブシステムを使用します。その仕組みを以下に説明します
- リアクティブステート: Interactivity API では、グローバルステートとローカルコンテキストの両方がリアクティブです。すなわち、これらのデータ元のいずれかが変更されると、依存する UI の任意の部分が自動的に更新されます。
- グローバルステート: インタラクティブブロック全体でアクセスできるグローバルデータ
- ローカルコンテキスト: 特定の要素とその子に固有のローカルデータ
- 派生ステート: 基本的なステートプロパティに加えて、依存関係が変更された際に自動で更新される計算型プロパティを定義できます。
Interactivity API での個々のリアクティブステートの使用方法については、グローバルステート、ローカルコンテキスト、派生ステートの理解を参照してください。
- アクション: 通常イベントハンドラによってトリガーされる関数。グローバルステートやローカルコンテキストを変更します。
- リアクティブバインディング: HTML 要素は、特別な属性を使用して、リアクティブステートの値にバインドされます。属性の例:
data-wp-bind
、data-wp-text
、data-wp-class
。
- 自動更新: アクションがグローバルステートまたはローカルコンテキストを変更すると、Interactivity API は、そのステートに依存する DOM のすべての部分を (直接または派生ステートを介して) 自動的に更新します。
先ほどの例をベースに、これらのコンセプトの詳細を見ていきます。
const { state } = store( 'myInteractivePlugin', {
state: {
isVisible: false,
isActive: false,
get visibilityText() {
return state.isVisible ? 'hide' : 'show';
},
// ... その他の派生ステート
},
actions: {
toggleVisibility() {
state.isVisible = ! state.isVisible;
},
// ... その他のアクション
},
} );
このコードでは、
isVisible
とisActive
は基本のステートプロパティvisibilityText
は派生ステートでisVisible
が変わると自動的に更新されるtoggleVisibility
はステートを変更するアクション
HTML バインディングは次のようになっています。
<button
data-wp-on--click="actions.toggleVisibility"
data-wp-text="state.visibilityText"
data-wp-bind--aria-expanded="state.isVisible"
>
show
</button>
ここでリアクティブが実際に動作します。
- ボタンをクリックすると
toggleVisibility
アクションがトリガーされる。 - このアクションは
state.isVisible
を更新する。 - Interactivity API はこの変更を検知し、自動的に以下を実行する。
- ボタンのテキストコンテンツを更新する (
data-wp-text="state.visibilityText"
のため)。 aria-expanded
属性を変更する (data-wp-bind--aria-expanded="state.isVisible"
のため)。isVisible
やvisibilityText
に依存する DOM の他の部分を更新する。
- ボタンのテキストコンテンツを更新する (
ミュータブル (可変) vs イミュータブル (不変)
他の多くのリアクティブフレームワークと異なり、Interactivity API はグローバルステートやローカルコンテキストの更新において、イミュータブル性を使用する必要はありません。オブジェクトや配列を直接変更しても、リアクティブシステムは期待どおりに動作します。これは多くの場合、より直感的でわかりやすいコードになります。
たとえば、次のように新しい項目を配列にプッシュできます。
const { state } = store( 'myArrayPlugin', {
state: {
list: [ 'item 1', 'item 2' ],
},
actions: {
addItem() {
// 正しい:
state.list.push( 'new item' );
// 間違い:
state.list = [ ...state.list, 'new item' ]; // こうしない !
},
},
} );
他のフレームワークのように新しい配列を作成したり、スプレッド演算子を使用する必要はありません。Interactivity API がこの変更を検出し、state.list
に依存する UI のすべての部分を更新します。
リアクティブの副作用 (side effects)
Interactivity API では UI の自動更新に加えて、data-wp-watch
などのディレクティブを使用して、リアクティブなデータが変更された際に副作用 (side effects) を実行できます。副作用はロギングや API 呼び出し、UI に直接関係しないアプリケーションの他の部分の更新などのタスクに便利です。
以下は data-wp-watch
の使用例です。
<div
data-wp-interactive="myCounterPlugin"
data-wp-context='{ "counter": 0 }'
data-wp-watch="callbacks.logCounter"
>
<p>Counter: <span data-wp-text="context.counter"></span></p>
<button data-wp-on--click="actions.increment">Increment</button>
</div>
store( 'myCounterPlugin', {
actions: {
increment() {
const context = getContext();
context.counter += 1;
},
},
callbacks: {
logCounter: () => {
const context = getContext();
console.log( `The counter is now: ${ context.counter }` );
},
},
} );
この例では、
data-wp-context
ディレクティブはローカルコンテキストを追加する。プロパティcounter
の値は0
。data-wp-watch
ディレクティブにcallbacks.logCounter
を設定する。context.counter
が変更されるたびに、logCounter
コールバックが実行される。logCounter
コールバックはコンソールに現在のカウンタをログする。
これにより、データの変更に応じて自動的に実行される、宣言的な副作用を作成できます。data-wp-watch
のその他の使用例としては、以下のような例が考えられます。
- データが変更されたら
localStorage
にデータを保存する。 - アナリティクスイベントを送信する。
- アクセシビリティのためにフォーカスを変更する。
- ページタイトル、メタタグ、
<body>
属性を更新する。 - アニメーションをトリガーする。
結論
Interactivity API を利用する際には、ステート、アクション、副作用の観点で考えることが重要です。データを定義し、それがどのように変化すべきかを記述したら、残りは Interactivity API に任せましょう。この発想の転換には時間がかかるかもしれません、特に、命令型のプログラミングスタイルに慣れている方には。しかし、これを受け入れることで、Interactivity API の可能性を最大限に引き出し、ユーザーに喜びを与える、真にダイナミックでインタラクティブなWordPress ブロックを作成できます。
命令形アプローチの例のバグの答え
最初に show ボタン、次に activate ボタン、最後に hide ボタンを押すと、statusParagraph.classList.add('inactive');
が呼ばれず inactive
クラスが追加されません。したがって、次にユーザーが show ボタンを押しても、段落は赤く表示されません。