グローバルステート、ローカルコンテキスト、派生ステートの理解

Interactivity API は、インタラクティブブロックを作成する強力なフレームワークを提供します。この機能を最大限、活用するにはグローバルステート、ローカルコンテキスト、派生ステートを使用するタイミングの理解が重要です。このガイドではこれらの概念を明らかにし、それぞれを使用するタイミングを決める際に役立つ実践的な例を提供します。

まず、グローバルステート、ローカルコンテキスト、派生ステートの簡単な定義から始めましょう。

  • グローバルステート: ページ上の任意のインタラクティブブロックからアクセス、変更可能なグローバルデータ。インラクティブブロックの異なるパーツの同期を維持できます。
  • ローカルコンテキスト: HTML 構造内の特定の要素内で定義されたローカルデータ。その要素と子要素のみアクセス可能で、それぞれのブロックが独立したステートを持ちます。
  • 派生ステート: グローバルステートやローカルコンテキストに基づいて計算される値。動的に必要に応じて計算され、冗長なデータを保存することなく一貫したデータ表現が保証されます。

それではそれぞれのコンセプトをより詳しく学習し、いくつかの例を見てみましょう。

グローバルステート

Interactivity API における グローバルステート とは、ページ上の任意のインタラクティブブロックからアクセス、変更可能なグローバルデータを指します。共有された情報ハブとして機能し、ブロックの異なるパーツが通信し、同期を維持できます。グローバルステートは、DOM ツリー内の位置に関係なく、インタラクティブブロック間で情報を交換する理想的なメカニズムです。

以下のような場面ではグローバルステートを使用してください。

  • DOM 階層内で直接関連していない複数のインタラクティブブロック間でデータを共有する必要がある。
  • すべてのインタラクティブブロックを越えて、特定のデータにおける、単一の真のソース (single source of truth) を維持したい。
  • UI の複数の部分に同時に影響するデータを扱っている。
  • ページに対してグローバルな機能を実装したい。

グローバルステートの操作

グローバルステートの初期化

通常、グローバルステートの初期値は wp_interactivity_state 関数を使用してサーバー上で定義します。

// グローバルステートの初期値を設定
wp_interactivity_state( 'myPlugin', array(
  'isDarkTheme' => true,
  'show'        => false,
  'helloText'   => __( 'world' ),
));

このグローバルステートの初期値は、PHP 内でページのレンダリングに使用され、HTML マークアップとなり、ブラウザに送信されます。

  • 開発者が PHP ファイル内に記述した HTML マークアップ:
<div
  data-wp-interactive="myPlugin"
  data-wp-class--is-dark-theme="state.isDarkTheme"
  class="my-plugin"
>
  <div data-wp-bind--hidden="!state.show">
    Hello <span data-wp-text="state.helloText"></span>
  </div>
  <button data-wp-on-async--click="actions.toggle">Toggle</button>
</div>

  • ディレクティブが処理され、ブラウザに送る準備ができた後の HTML マークアップ:
<div
  data-wp-interactive="myPlugin"
  data-wp-class--is-dark-theme="state.isDarkTheme"
  class="my-plugin is-dark-theme"
>
  <div hidden data-wp-bind--hidden="!state.show">
    Hello <span data-wp-text="state.helloText">world</span>
  </div>
  <button data-wp-on-async--click="actions.toggle">Toggle</button>
</div>

ディレクティブがサーバ上でどのように処理されるかについては、サーバサイドレンダリングガイドを参照してください。

PHP 内でのページのレンダリング中にグローバルステートを使用しない場合は、直接クライアントでも定義できます。

const { state } = store( 'myPlugin', {
  state: {
    isLoading: false,
  },
  actions: {
    *loadSomething() {
      state.isLoading = true;
      // ...
    },
  },
} );

注意: これは正しく動作しますが、一般的なベストプラクティスとしてはすべてのグローバルステートをサーバー上で定義してください。

グローバルステートへのアクセス

HTML マークアップ内のディレクティブ属性の値では state を参照することでグローバルステートの値に直接アクセスできます。

<div data-wp-bind--hidden="!state.show">
  <span data-wp-text="state.helloText"></span>
</div>

JavaScript では、@wordpress/interactivity パッケージの store 関数が設定、取得の両方で機能し、選択されたネームスペースのストアを返します。

アクションやコールバックでグローバルステートにアクセスするには、 store 関数が返すオブジェクトの state プロパティを使用します。

const myPluginStore = store( 'myPlugin' );

myPluginStore.state; // これは名前空間 myPlugin のストア

store が返すオブジェクトは分割も可能です。

const { state } = store( 'myPlugin' );

そして同じことは同時にストアを定義しながら行うこともでき、これが最も一般的な使用方法です。

const { state } = store( 'myPlugin', {
  state: {
    // ...
  },
  actions: {
    toggle() {
      state.show = ! state.show;
    },
  },
} );

wp_interactivity_state 関数を使用してサーバー上で初期化されたグローバルステートも、サーバーからクライアントに自動的にシリアライズされるため、オブジェクトに含まれます。

wp_interactivity_state( 'myPlugin', array(
  'someValue' => 1,
));

const { state } = store( 'myPlugin', {
  state: {
    otherValue: 2,
  },
  actions: {
    readGlobalState() {
      state.someValue; // これは存在して、初期値は1
      state.otherValue; // これも存在して、初期値は2
    },
  },
} );

最後に、同じ名前空間を持つすべての store 関数呼び出しはマージされます。

store( 'myPlugin', { state: { someValue: 1 } } );

store( 'myPlugin', { state: { otherValue: 2 } } );

/* すべての `store` 呼び出しは同じオブジェクトへの安定した参照 (stable reference) を返す。 このため、
  * どの呼び出しからでも `state` への参照を取得できる */
const { state } = store( 'myPlugin' );

store( 'myPlugin', {
  actions: {
    readValues() {
      state.someValue; // 存在して、初期値は1
      state.otherValue; // 存在して、初期値は2
    },
  },
} );

グローバルステートの更新

グローバルステートを更新するために必要な作業は、一度 store 関数から取得した state オブジェクトを変更 (mutate) するだけです。

const { state } = store( 'myPlugin', {
  actions: {
    updateValues() {
      state.someValue = 3;
      state.otherValue = 4;
    },
  },
} );

グローバルステートを変更すると、変更した値に依存する任意のディレクティブ内の変更が自動的にトリガーされます。

Interactivity API 内でリアクティブがどのように動作するかの詳細については「リアクティブと宣言型の考え方」を参照してください。

例: グローバルステートを使用して通信する2つのインタラクティブブロック

この例には2つの独立したインタラクティブブロックがあります。1つはカウンターを表示し、もう1つにはそのカウンターを増分するボタンがあります。この2つのブロックは、HTML の構造に関係なくページのどこにでも配置できます。言い換えれば、一方が他方の内部ブロックである必要はありません。

カウンターブロック

<?php
wp_interactivity_state( 'myCounterPlugin', array(
  'counter' => 0
));
?>

<div
  data-wp-interactive="myCounterPlugin"
  <?php echo get_block_wrapper_attributes(); ?>
>
  Counter: <span data-wp-text="state.counter"></span>
</div>

インクリメントブロック

<div
  data-wp-interactive="myCounterPlugin"
  <?php echo get_block_wrapper_attributes(); ?>
>
  <button data-wp-on-async--click="actions.increment">
    Increment
  </button>
</div>

const { state } = store( 'myCounterPlugin', {
  actions: {
    increment() {
      state.counter += 1;
    },
  },
} );

この例では

  1. グローバルステートは wp_interactivity_state を使用してサーバー上で初期化され、初期値として counter を 0 に設定します。
  2. カウンターブロックは、data-wp-text="state.counter" を使用して現在のカウンターを、グローバルステートから読み取り、表示します。
  3. インクリメントブロックにはクリックすると increment アクションをトリガーするボタンがあり、これには data-wp-on-async--click="actions.increment" を使用します。
  4. JavaScript 内で increment アクションは state.counter を増分することで、グローバルステートを直接変更します。

2つのブロックは独立していてページのどこにでも配置できます。互いを入れ子にしたり、DOM 構造で直接関連づける必要はありません。これらのインタラクティブブロックの複数のインスタンスをページに追加でき、すべて同じグローバルなカウンターの値を共有し、更新します。

ローカルコンテキスト

Interactivity API における ローカルコンテキスト とは、HTML 構造内の特定の要素内で定義されたローカルデータを指します。グローバルステートとは異なりローカルコンテキストは、そのローカルコンテキストを定義した要素とその子要素からのみアクセスできます。

ローカルコンテキストは、個々のインタラクティブブロックで、独立したステートが必要な場合に特に有用です。ブロックの各インスタンスは、他のインスタンスと干渉しない自身のデータを維持できます。

以下のような場面ではローカルコンテキストを使用してください。

  • あるインタラクティブブロックの複数のインスタンスに対して、それぞれ個別のステートを保持する必要がある。
  • あるインタラクティブブロックとその子にのみ関連するデータをカプセル化したい。
  • UI の特定の部分に分離された機能を実装する必要がある。

ローカルコンテキストの操作

ローカルコンテキストの初期化

ローカルコンテキストは data-wp-context ディレクティブを使用して HTML 構造の中で直接、初期化します。このディレクティブはコンテキストの初期値を定義する JSON 文字列を受け付けます。

<div data-wp-context='{ "counter": 0 }'>
  <!-- 子要素は `context.counter` にアクセスできる -->
</div>

またローカルコンテキストは wp_interactivity_data_wp_context PHP ヘルパーを使用して、サーバー上でも初期化できます。このヘルパーは文字列化した値の適切なエスケープとフォーマットを保証します。

<?php
$context = array( 'counter' => 0 );
?>

<div <?php echo wp_interactivity_data_wp_context( $context ); ?>>
  <!-- 子要素は `context.counter` にアクセスできる -->
</div>

ローカルコンテキストへのアクセス

HTML のマークアップ内では、ディレクティブの値で context を参照することで、ローカルコンテキストの値に直接アクセスできます。

<div data-wp-bind--hidden="!context.isOpen">
  <span data-wp-text="context.counter"></span>
</div>

JavaScript では、getContext関数を使用してローカルコンテキストの値にアクセスできます。

store( 'myPlugin', {
  actions: {
    sendAnalyticsEvent() {
      const { counter } = getContext();
      myAnalyticsLibrary.sendEvent( 'updated counter', counter );
    },
  },
  callbacks: {
    logCounter() {
      const { counter } = getContext();
      console.log( `Current counter: ${ counter }` );
    },
  },
} );

getContext 関数はアクションやコールバックの実行をトリガーした要素のローカルコンテキストを返します。

ローカルコンテキストの更新

JavaScript 内でローカルコンテキストの値を更新するには、getContext が返すオブジェクトを変更します。

store( 'myPlugin', {
  actions: {
    increment() {
      const context = getContext();
      context.counter += 1;
    },
    updateName( event ) {
      const context = getContext();
      context.name = event.target.value;
    },
  },
} );

ローカルコンテキストを変更すると、変更した値に依存する任意のディレクティブ内での変更が自動的にトリガーされます。

Interactivity API 内でリアクティブがどのように動作するかの詳細については「リアクティブと宣言型の考え方」を参照してください。

ローカルコンテキストの入れ子

ローカルコンテキストは入れ子にでき、子コンテキストは親コンテキストの値を継承し、必要であれば上書きできます。

<div data-wp-context='{ "theme": "light", "counter": 0 }'>
  <p>Theme: <span data-wp-text="context.theme"></span></p>
  <p>Counter: <span data-wp-text="context.counter"></span></p>

  <div data-wp-context='{ "theme": "dark" }'>
    <p>Theme: <span data-wp-text="context.theme"></span></p>
    <p>Counter: <span data-wp-text="context.counter"></span></p>
  </div>
</div>

この例では、内側の div は theme 値として "dark" を持ちますが、counter 値は親コンテキストから 0 を継承します。

例: ローカル・コンテキストを使用して独立したステートを持つ1つのインタラクティブブロック

この例には1つのインタラクティブブロックがあり、カウンターを表示し、これを増分できます。ローカルコンテキストを使うことでこのブロックの各インスタンスは、複数のブロックがページに追加されても、それぞれ独立したカウンターを持ちます。

<div
  data-wp-interactive="myCounterPlugin"
  <?php echo get_block_wrapper_attributes(); ?>
  data-wp-context='{ "counter": 0 }'
>
  <p>Counter: <span data-wp-text="context.counter"></span></p>
  <button data-wp-on-async--click="actions.increment">Increment</button>
</div>

store( 'myCounterPlugin', {
	actions: {
		increment() {
			const context = getContext();
			context.counter += 1;
		},
	},
} );

この例では、

  1. 初期値 0 の ローカルコンテキスト counter を data-wp-context ディレクティブを使用して定義する。
  2. カウンタは data-wp-text="context.counter" を使用して表示され、ローカルコンテキストから値を読み込む。
  3. インクリメントボタンは data-wp-on-async--click="actions.increment" を使用して increment アクションをトリガーする。
  4. JavaScript では、getContext 関数を使用して、各ブロックインスタンスのローカルコンテキストにアクセスして変更する。

ユーザーはこのブロックの複数のインスタンスをページに追加でき、各インスタンスはそれぞれの独立したカウンターを保持します。あるブロックの Increment ボタンをクリックすると、その特定のブロックのカウンターのみ増分し、他のブロックには影響しません。

派生ステート

Interactivity API における 派生ステート とは、グローバルステートまたはローカルコンテキストの他の部分から計算される値を指します。値は必要に応じて計算され、保存されません。これにより一貫性が保証され、冗長性が減り、コードの宣言的な性質が強化されます。

派生ステートはモダンなステート管理における基本的な概念で、Interactivity API に固有ではありません。他の一般的なステート管理システムでも使用されていて、Redux では selectors、Preact Signals では computed 値と呼ばれます。

派生ステートにはアプリケーションステートを適切に設計するために不可欠な、いくつかの重要な利点があります。

  1. 単一の真のソース: 派生ステートは本質的な、生のデータのみをステート内に保存することを推進します。核となるデータから計算できる値は、派生ステートにできます。このアプローチにより、インタラクティブブロック内での不整合のリスクを低減できます。
  1. 自動更新: 派生ステートを使用するとき、値は、元となるデータが変更されるたびに自動的に再計算されます。これにより、インタラクティブブロックのすべての部分が、手動で操作することなく、常に最新の情報にアクセスできます。
  1. ステート管理の簡素化: 手動で値を保存し、更新するのではなく、必要に応じて値を計算することで、ステート管理ロジックの複雑さを軽減できます。これは、よりクリーンで保守性の高いコードにつながります。
  1. パフォーマンスの向上: 多くの場合、派生ステートは必要な場合にのみ再計算するよう最適化でき、インタラクティブブロックのパフォーマンスを向上する可能性があります。
  1. デバッグの容易性: 派生ステートでは、データがどこから来て、どのように変換されたが明確です。このため、インタラクティブブロック内の問題を追跡しやすくなります。

つまり派生ステートでは、インタラクティブブロック内の異なるデータ間の関係を宣言的に表現できます。何かが変更されるたびに、関連する値を強制的に更新する必要はありません。

Interactivity API の宣言的コーディングの活用について学習するには「リアクティブと宣言型の考え方」を参照してください。

以下のような場面では派生ステートを使用してください。

  • グローバルステートやローカルコンテキストの一部が、他のステート値から計算できる。
  • 手動で同期を保つ必要のある冗長なデータを避けたい。
  • 派生値を自動的に更新することで、インタラクティブブロック全体での一貫性を確保したい。
  • 関連する複数のステートプロパティを更新する必要性を失くすことで、アクションを単純化したい。

派生ステートの操作

派生ステートの初期化

通常、派生ステートはグローバルステートとまったく同じように wp_interactivity_state 関数を使用して、サーバー上で初期化する必要があります。

  • 初期値が分かっていて、静的な場合は、直接定義できます。
wp_interactivity_state( 'myCounterPlugin', array(
  'counter' => 1, // これはグローバルステート
  'double'  => 2, // これは派生ステート
));

  • あるいは、必要な計算をして定義できます。
$counter = 1;
$double  = $counter * 2;

wp_interactivity_state( 'myCounterPlugin', array(
  'counter' => $counter, // これはグローバルステート
  'double'  => $double,  // これは派生ステート
));

どのようなアプローチにせよ、PHP でページをレンダリングする際には、派生ステートの初期値が使用され、HTML に正しい値を挿入できます。

ディレクティブがサーバ上でどのように処理されるかについては、サーバサイドレンダリングガイドを参照してください。

派生ステートプロパティがローカルコンテキストに依存する場合でも、同じメカニズムが適用されます。

<?php
$counter = 1;

// これはローカルコンテキスト
$context = array( 'counter' => $counter );

wp_interactivity_state( 'myCounterPlugin', array(
  'double' => $counter * 2, // これは派生ステート
));
?>

<div
  data-wp-interactive="myCounterPlugin"
  <?php echo wp_interactivity_data_wp_context( $context ); ?>
>
  <div>
    Counter: <span data-wp-text="context.counter"></span>
  </div>
  <div>
    Double: <span data-wp-text="state.double"></span>
  </div>
</div>

JavaScript で派生ステートは getter を使用して定義します。

const { state } = store( 'myCounterPlugin', {
  state: {
    get double() {
      return state.counter * 2;
    },
  },
} );

派生ステートはローカルコンテキストに依存でき、また、ローカルコンテキストとグローバルステートに同時に依存できます。

const { state } = store( 'myCounterPlugin', {
  state: {
    get double() {
      const { counter } = getContext();
      // ローカルコンテキストに依存
      return counter * 2;
    },
    get product() {
      const { counter } = getContext();
      // ローカルコンテキストとグローバルステートに依存
      return counter * state.factor;
    },
  },
} );

派生ステートがローカルコンテキストに依存し、ローカルコンテキストがサーバー内で動的に変化する場合、派生ステートの初期値の代わりとして、動的に派生ステートを計算する関数 (クロージャ) を使用できます。

<?php
wp_interactivity_state( 'myProductPlugin', array(
  'list'    => array( 1, 2, 3 ),
  'factor'  => 3,
  'product' => function() {
    $state   = wp_interactivity_state();
    $context = wp_interactivity_get_context();
    return $context['item'] * $state['factor'];
  }
));
?>

<template
  data-wp-interactive="myProductPlugin"
  data-wp-each="state.list"
>
  <span data-wp-text="state.product"></span>
</template>

この data-wp-each テンプレートは、以下の HTML をレンダーします (ディレクティブは省略)。

<span>3</span>
<span>6</span>
<span>9</span>

派生ステートへのアクセス

HTML のマークアップ内で、派生ステートの構文はグローバルステートの構文と同じです。ディレクティブ属性の値内で state を参照するだけです。

<span data-wp-text="state.double"></span>

JavaScript 内でも同じです。グローバルステートも派生ステートもストアの state プロパティを通して使用できます。

const { state } = store( 'myCounterPlugin', {
  // ...
  actions: {
    readValues() {
      state.counter; // 通常のステート。1 を返す
      state.double; // 派生ステート。2 を返す
    },
  },
} );

両者に違いがないのは意図的です。開発者は派生ステートとグローバルステートの両方を同じように利用できるため、実用的な意味で互換性があります。

また、派生ステートは別の派生ステートからもアクセスできるため、計算値の複数のレベルを作成できます。

const { state } = store( 'myPlugin', {
  state: {
    get double() {
      return state.counter * 2;
    },
    get doublePlusOne() {
      return state.double + 1;
    },
  },
} );

派生ステートの更新

派生ステートは直接、更新できません。その値を更新するには、派生ステートが依存するグローバルステートまたはローカルコンテキストを更新する必要があります。

const { state } = store( 'myCounterPlugin', {
  // ...
  actions: {
    updateValues() {
      state.counter; // 通常のステート。1 を返す
      state.double; // 派生ステート。2 を返す

      state.counter = 2;

      state.counter; // 通常のステート。2 を返す
      state.double; // 派生ステート。4 を返す
    },
  },
} );

例: 派生ステートを使用しない例と使用する例

ここでカウンターを持ち、その2倍の値を表示する例を考え、派生ステートを使用しない場合と、使用する場合の2つのアプローチを比較します。

派生ステートを使用しない例

const { state } = store( 'myCounterPlugin', {
  state: {
    counter: 1,
    double: 2,
  },
  actions: {
    increment() {
      state.counter += 1;
      state.double = state.counter * 2;
    },
  },
} );

このアプローチでは、state.counter と state.double の両方の値を increment アクション内で手動で更新します。機能は動作しますが、いくつかの欠点があります。

  • 宣言的でない。
  • 複数の場所で state.counter が更新され、開発者が state.double の同期を忘れるとバグにつながる。
  • 関連する値の更新を覚えておく必要があるため、認知的な負荷が高い。

派生ステートを使用する例

const { state } = store( 'myCounterPlugin', {
  state: {
    counter: 1,
    get double() {
      return state.counter * 2;
    },
  },
  actions: {
    increment() {
      state.counter += 1;
    },
  },
} );

この改良版では、

  • state.double は getter として定義され、その値は自動的に state.counter から取得される。
  • increment アクションは state.counter を更新するだけでよい。
  • state.double は、state.counter がどこで、どのように更新されても、常に正しい値を持つことが保証される。

例: 派生ステートをローカルコンテキストと使用する

次に、カウンターを初期化するローカルコンテキストの例を考えます。

store( 'myCounterPlugin', {
	state: {
		get double() {
			const { counter } = getContext();
			return counter * 2;
		},
	},
	actions: {
		increment() {
			const context = getContext();
			context.counter += 1;
		},
	},
} );

<div data-wp-interactive="myCounterPlugin">
	<!-- "Double: 2" をレンダーする -->
	<div data-wp-context='{ "counter": 1 }'>
		Double: <span data-wp-text="state.double"></span>

		<!-- このボタンはローカルカウンターを増分する -->
		<button data-wp-on-async--click="actions.increment">Increment</button>
	</div>

	<!-- "Double: 4" をレンダーする -->
	<div data-wp-context='{ "counter": 2 }'>
		Double: <span data-wp-text="state.double"></span>

		<!-- このボタンはローカルカウンターを増分する -->
		<button data-wp-on-async--click="actions.increment">Increment</button>
	</div>
</div>

この例では、派生ステート state.double は、各要素に存在するローカルコンテキストを読み込み、使用されるインスタンスに応じた正しい値を返します。

例: 派生ステートをローカルコンテキストとグローバルステートの両方と使用する

次に、グローバルの税率とローカルの商品価格があるときに、税込みの最終価格を計算する例を考えます。

<div
	data-wp-interactive="myProductPlugin"
	data-wp-context='{ "priceWithoutTax": 100 }'
>
	<p>Product Price: $<span data-wp-text="context.priceWithoutTax"></span></p>
	<p>Tax Rate: <span data-wp-text="state.taxRatePercentage"></span></p>
	<p>Price (inc. tax): $<span data-wp-text="state.priceWithTax"></span></p>
</div>

const { state } = store( 'myProductPlugin', {
	state: {
		taxRate: 0.21,
		get taxRatePercentage() {
			return `${ state.taxRate * 100 }%`;
		},
		get priceWithTax() {
			const { priceWithoutTax } = getContext();
			return price * ( 1 + state.taxRate );
		},
	},
	actions: {
		updateTaxRate( event ) {
			// グローバルの税率を更新する
			state.taxRate = event.target.value;
		},
		updatePrice( event ) {
			// ローカルの商品価格を更新する
			const context = getContext();
			context.priceWithoutTax = event.target.value;
		},
	},
} );

この例では、priceWithTax はグローバルな taxRate とローカルの priceWithoutTax の両方から派生しています。updateTaxRate アクションや updatePrice アクションでグローバルステートやローカルコンテキストを更新するたびに、Interactivity API は派生ステートを再計算し、DOM の必要な部分を更新します。

派生ステートを使用することでコードベースは、より保守性が高く、エラーが起こりにくくなります。関連するステートの値は常に同期していることが保証され、アクションの複雑さは減り、コードはより宣言的で推測しやすくなります。

まとめ

効率的なステート管理のポイントは、ステートを最小限に保ち、冗長性を避けることです。派生ステートを使用して動的に値を計算し、データのスコープと要件に基づいてグローバルステートとローカルコンテキストを選択してください。この結果、デバッグや保守がしやすく、よりクリーンで堅牢なアーキテクチャが導かれます。

原文

s
検索
c
新規投稿を作成する
r
返信
e
編集
t
ページのトップへ
j
次の投稿やコメントに移動
k
前の投稿やコメントに移動
o
コメントの表示を切替
esc
投稿やコメントの編集をキャンセル