WordPressアプリケーションの状態(データの扱い方や整理方法)管理は、複雑なタスクです。プロジェクトの規模が大きくなるにつれ、データの流れを追跡し、コンポーネント間で一貫したアップデートを保つことはより難しくなります。そこで活用したいのが、状態管理のための堅牢なソリューションとなるWordPressのデータパッケージ(@wordpress/data)です。

今回は、このWordPressのデータパッケージについて掘り下げ、概念と実装戦略、ベストプラクティスをご紹介します。

@wordpress/dataとは

WordPressのデータパッケージである@wordpress/dataは、JavaScript(ES2015以降)の状態管理ライブラリで、アプリケーションの状態を一元管理し、効率的かつ予測可能な方法でデータを操作できます。適切に実装することで、複雑なユーザーインターフェースの構築や、アプリケーション全体のデータフローの処理を簡素化することができます。

@wordpress/dataは、Reactエコシステムで人気の状態管理フレームワーク、Reduxから着想を得ています。

Reduxの公式サイト
Reduxの公式サイト

このデータモジュールはWordPress環境内で動作し、WordPress固有の機能やAPIと統合されています。WordPressのブロックエディター向けに開発を行う場合、またはブロックエディターをサポートしなければならない場合、このモジュールはその状態管理において重要な役割を果たします。開発するプラグインやテーマで同じツールやパターンを使用することで、より一貫性のある使いやすい開発環境を作ることができます。

@wordpress/dataとReduxの関係性

@wordpress/dataは、Reduxから着想を得ていますが、Reduxをそのまま移植したわけではありません。WordPressのエコシステムに合わせて多数の改良が加えられており、Reduxと比較して以下のような利点があります。

  • WordPressのAPIや機能とシームレスに動作するように設計されている(標準的なReduxでは調整が必要)。
  • より効率的なAPIを提供するため、簡単に使用することができる。
  • 非同期アクションのサポートが組み込まれており、WordPress REST APIを扱う場合に便利。

また、@wordpress/dataREST APIと比較することもできます。どちらもデータ管理を扱いますが、それぞれ異なる目的があります。

  • WordPress REST APIは、HTTP経由でWordPressのデータとやりとりする方法を提供し、外部アプリやヘッドレスWordPressのセットアップなど、データの取得や操作が必要になる場合に使用する。
  • @wordpress/dataは、データとUIの状態を一元的に保存し、アプリ内でデータの流れや更新を処理する。

多くの場合、サーバー上のデータを取得および更新するREST API と、アプリケーション内でデータを管理する@wordpress/dataの両方を使用することになります。

@wordpress/dataの主な概念と用語

@wordpress/dataでは、直感的に状態管理を行うことができます。ここで言う「状態」とは、ストア内のデータを指します。アプリケーションの現在の状態を表し、UIの状態(モーダルが開いているかどうかなど)やデータの状態(投稿一覧など)を含めることができます。

投稿データの状態は、WordPressのデータパッケージが管理する領域の1つ
投稿データの状態は、WordPressのデータパッケージが管理する領域の1つ

この文脈では、「ストア」は@wordpress/dataの中心的な役割を果たします。サイト全体の状態を保持し、その状態にアクセスしたり更新したりするための方法を提供します。WordPressでは、複数のストアを持つことができ、それぞれがサイトの特定の領域を担当します。

ストアを管理するには、レジストリが必要になります。レジストリはストアを登録したり、既存のストアにアクセスしたりするための方法を提供するもので、レジストリがストアを保持し、ストアがアプリケーションの状態を保持します。

状態管理にはいくつかの重要な概念があります。

  • アクション(Action):状態の変更を記述する。アクションはプレーンなJavaScriptオブジェクトで、状態の更新をトリガーする唯一の手段。通常はtypeプロパティを持ち、追加データを含む場合もある。
  • セレクタ(Selector):ストアから状態の特定の部分を抽出する。この関数を使用すると、ストアの構造を直接操作することなくデータにアクセスできる。リゾルバはこれに関連し、非同期データの取得を処理。セレクタを実行する前に、ストアの必要なデータにアクセスできるかを確認するために使用する。
  • リデューサー(Reducer):アクションに応じて状態をどのように変化させるかを指示する。現在の状態とアクションを引数にとり、新たな状態オブジェクトを返す。制御関数は、複雑な非同期処理を副作用なく処理できるようにする。

これらの基本的な概念は、ストアを中心とした堅牢な状態管理システムの構築に関係するため、理解しておきましょう。

@wordpress/dataの中核となるストア

ストアはアプリケーションの状態を格納するコンテナであり、状態を操作するための方法を提供します。@wordpress/dataには他にもいくつかのパッケージが組み込まれており、それぞれブロックディレクトリ、ブロックエディター、コア、投稿編集などのストアが登録されています。

各ストアはcorecore/editorcore/noticesのように、固有の名前空間を持ちます。サードパーティプラグインもストアを登録するため、競合を避けるために一意の名前空間を選択する必要があります。いずれにしても、登録したストアはほとんどの場合、デフォルトのレジストリに登録されます。

この中核となるレジストリは以下のような役割を担います。

  • ストアの新規登録
  • 既存ストアへのアクセス
  • 状態の変更に対する購読の管理

レジストリと直接やりとりすることはあまりありませんが、@wordpress/dataがWordPress全体の状態管理をどのようにオーケストレーションするかに関して、レジストリの役割は理解しておきましょう。

WordPressデータストアとの基本的なやり取り

ES2015以降のJavaScriptを使用していて、WordPressのプラグインやテーマを使用している場合は、これを依存関係として含めることができます。

npm install @wordpress/data --save

コード内で、ファイルの先頭でパッケージから必要な関数をインポートします。

import { select, dispatch, subscribe } from '@wordpress/data';

既存のWordPressストアとやりとりするには、インポートした関数を使用します。例えば、selectを使って状態データにアクセスします。

const posts = select('core').getPosts();

アクションのディスパッチも同様です。

dispatch('core').savePost(postData);

状態の変更を購読(監視)する場合は、少し異なる形式を使用しますが、考え方は同じです。

subscribe(() => {
  const newPosts = select('core').getPosts();
  // 新規投稿に基づいてUIを更新
});

ただし、常にデフォルトのストアで作業するわけではなく、多くの場合は、追加した既存のストアを使用するか、独自のストアを登録します。

WordPressデータストアの登録方法

ストアの設定を定義し、@wordpress/dataに登録するには、まずregister関数をインポートします。

…
import { createReduxStore, register } from '@wordpress/data';
…

この関数は1つの引数、ストア記述子を取ります。続いて、ストアのデフォルト状態を定義し、デフォルト値を設定します。

…
const DEFAULT_STATE = {
  todos: [],
};
…

actionsオブジェクトを作成し、Reducer関数を定義して状態の更新を処理し、selectorsオブジェクトを作成して状態データにアクセスする関数を定義します。

const actions = {
  addTodo: (text) => ({
    type: 'ADD_TODO',
    text,
  }),
};

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
      ...state,
      todos: [...state.todos, { text: action.text, completed: false }],
      };
    default:
      return state;
   }
};

const selectors = {
  getTodos: (state) => state.todos,
};

ストアの設定を作成するには、createReduxStoreオブジェクトを使用して定義します。これにより、ストアのアクション、セレクタ、コントロール、その他のプロパティが初期化されます。

const store = createReduxStore('my-plugin/todos', {
  reducer,
  actions,
  selectors,
});

このオブジェクトには、少なくとも状態の形と他のアクションに反応してどのように変化するかを定義するリデューサーが必要になります。最後に、createReduxStoreで定義したストア記述子を呼び出して、ストアを登録します。

register(store);

これで、他のストアと同じように操作できるようになります。

import { select, dispatch } from '@wordpress/data';
// 新規Todoを追加
dispatch('my-plugin/todos').addTodo('Learn WordPress data package');
// すべてのTodoを取得
const todos = select('my-plugin/todos').getTodos();

@wordpress/dataを使用する上で重要になるのは、さまざまなプロパティやオブジェクトの使用方法です。

WordPressデータストアの5つのプロパティ

@wordpress/dataを使用する際の作業の多くは、「逆向き」に進行します。つまり、ストア自体を定義する前に、低レベルのデータストアプロパティを定義しなければなりません。createReduxStoreオブジェクトはその代表例で、ストアの登録に使用する記述子を作成するために、定義したすべての要素をまとめます。

import { createReduxStore } from '@wordpress/data';
  const store = createReduxStore( 'demo', {
    reducer: ( state = 'OK' ) => state,
    selectors: {
    getValue: ( state ) => state,
    },
  } );

その他のプロパティについても、セットアップと設定が必要です。

1. アクション(Action)

アクションは、ストアの状態変更をトリガーする主な手段です。JavaScriptのオブジェクトで、その名の通り「何をするべきか」を記述します。どのような状態を取得したいかを決めることができるため、まずはアクションを作成するのがおすすめです。

const actions = {
  addTodo: (text) => ({
    type: 'ADD_TODO',
    text,
  }),
  toggleTodo: (index) => ({
    type: 'TOGGLE_TODO',
    index,
  }),
};

ActionCreatorsはオプションの引数を取り、定義したReducerに渡すオブジェクトを返します。

const actions = {
  updateStockPrice: (symbol, newPrice) => {
  return {
    type: 'UPDATE_STOCK_PRICE',
    symbol,
    newPrice
  };
},

ストアの記述子を渡すと、ActionCreatorsをディスパッチして状態の値を更新することができます。

dispatch('my-plugin/todos').updateStockPrice('¥', '150.37');

アクションオブジェクトは、状態を変更する方法に関するリデューサーへの指示だと考えることができます。少なくとも、作成、更新、読み込み、削除(CRUD)アクションを定義する必要があるでしょう。また、特に定数として定義する場合は、アクションタイプを個別のJavaScriptファイルにまとめ、それらすべてのタイプのオブジェクトを作成することもできます。

2. リデューサー(Reducer)

リデューサーはアクションと並んで中心的な役割を果たし、アクションからの指示に応じて状態をどのように変化させるかを指定します。アクションからの指示と現在の状態を渡すと、新たな状態オブジェクトを返し、それをチェーンに沿って渡すことができます。

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo, index) =>
          index === action.index ? { ...todo, completed: !todo.completed } : todo
        ),
    };
    default:
      return state;
    }
};

なお、リデューサーは純粋な関数でなければならず、受け取った状態を変化させるべきでないことは念頭においてください(むしろ更新して返すべき)。リデューサーとアクションは多くの点で共生関係にあるため、両者がどのように連携して動作するかは把握しておくことが重要です。

3. セレクタ(Selector)

登録されたストアから現在の状態にアクセスするには、セレクタが必要になります。ストアの状態を「公開」する主な方法であり、ストアの内部構造からコンポーネントを切り離すのに役立ちます。

const selectors = {
  getTodos: (state) => state.todos,
  getTodoCount: (state) => state.todos.length,
};

セレクタは、select関数で呼び出すことができます。

const todoCount = select('my-plugin/todos').getTodoCount();

ただし、セレクタはデータをどこかに送るわけではなく、単にデータを提示してアクセスを提供するのみになります。

セレクタは、状態に正確にアクセスするために必要な数の引数を受け取ることができ、セレクタが返す値は、定義したセレクタ内で引数が達成した結果になります。アクションと同様に、数が多くなる可能性があるため、すべてのセレクタを格納する別のファイルを用意するのもありです。

4. コントロール(Control)

サイトの機能の実行フローをガイドする、あるいはその中でロジックを実行するには、コントロールを使用します。アクションの実行フローの動作を定義し、リゾルバに渡す状態を収集するための仲介役として動作するため、@wordpress/dataのアシスタントのような存在です。

また、API呼び出しやブラウザAPIとのやりとりのようなストアの副作用も処理します。コントロールは、複雑な非同期処理を可能にしながら、リデューサーをクリーンな状態に保ちます。

const controls = {
  FETCH_TODOS: async () => {
    const response = await fetch('/api/todos');
    return response.json();
  },
};

const actions = {
  fetchTodos: () => ({ type: 'FETCH_TODOS' }),
};

データを取得して返すこのサイクルは、プロセス全体にとって重要です。しかし、アクションからの呼び出しがなければ、そのデータを使うことはできません。

5. リゾルバ(Resolver)

先に触れたとおり、セレクタはストアの状態を公開するものの、そのデータをどこにも送信しません。リゾルバは、セレクタ(およびコントロール)と連携してデータを取得します。また、非同期のデータ取得も扱うことができます。

const resolvers = {
  getTodos: async () => {
    const todos = await controls.FETCH_TODOS();
    return actions.receiveTodos(todos);
  },
};

リゾルバは、セレクタを実行する前に、要求されたデータがストアにあるかどうかを確認します。このリゾルバとセレクタの緊密な接続には、@wordpress/dataがリクエストされたデータに基づき、どのリゾルバを呼び出すべきかを把握するために名前の一致が不可欠になります。

さらに、リゾルバは常にセレクタ関数に渡すのと同じ引数を受け取り、アクションオブジェクトを返すか、生成するか、ディスパッチします。

@wordpress/data使用時のエラー処理

@wordpress/dataを扱う際には、適切なエラー処理(エラーハンドリング)を実装しなければなりません。非同期操作やフルスタックのデプロイ、API呼び出しを行う場合は特に重要です。

たとえば、非同期操作を含むアクションをディスパッチする場合、try-catchブロックを使用するのが得策です。

const StockUpdater = () => {
  // ディスパッチ機能の取得
  const { updateStock, setError, clearError } = useDispatch('my-app/stocks');
  const handleUpdateStock = async (stockId, newData) => {
    try {
      // 既存のエラーを消去
      clearError();
      // 在庫の更新を試行
      await updateStock(stockId, newData);
    } catch (error) {
      // 何か問題が発生した場合、エラーアクションをディスパッチ
      setError(error.message);
    }
};

  return (
    <button onClick={() => handleUpdateStock('AAPL', { price: 150 })}>
      Update Stock
    </button>
  );
};

Reducerの場合は、エラーアクションを処理して、状態を更新することができます。

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    // ... その他の場合
    case 'FETCH_TODOS_ERROR':
      return {
      ...state,
      error: action.error,
      isLoading: false,
    };
    default:
      return state;
  }
};

セレクタを使用する場合、潜在的な問題を処理するためにエラーチェックを含めることができます。データを使用する前にコンポーネントのエラーをチェックします。

const MyComponent = () => {
  // エラー情報を含む複数の状態を取得
  const { data, isLoading, error } = useSelect((select) => ({
    data: select('my-app/stocks').getStockData(),
    isLoading: select('my-app/stocks').isLoading(),
    error: select('my-app/stocks').getError()
  }));

  // さまざまな状態を扱う
  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return (
      <div className="error-message">
        <p>Error loading stocks: {error.message}</p>
        <button onClick={retry}>Try Again</button>
      </div>
    );
  }
  return (
    <div>
      {/* 通常のコンポーネントレンダリング */}
    </div>
  );
};

useSelectuseDispatchは、独自のエラーメッセージを因数として渡すことができるため便利です。

初期設定時にエラー状態を中心に管理することは良い習慣であり、エラー境界はコンポーネントレベルで保持することが推奨されます。また、読み込み状態のエラー処理を採用することも、コードを明快かつ一貫性のあるものに保つのに有用です。

WordPressデータストアをサイトに統合する方法

@wordpress/dataには、状態管理に役立つ要素が多数あります。これらをすべて統合することを検討してみてください。以下、リアルタイムで財務データを表示し、更新する株式ティッカーを見てみましょう。

まず最初に、データのストアを作成します。

import { createReduxStore, register } from '@wordpress/data';

const DEFAULT_STATE = {
  stocks: [],
  isLoading: false,
  error: null,
};

const actions = {
  fetchStocks: () => async ({ dispatch }) => {
  dispatch({ type: 'FETCH_STOCKS_START' });
  try {
    const response = await fetch('/api/stocks');
    const stocks = await response.json();
    dispatch({ type: 'RECEIVE_STOCKS', stocks });
  } catch (error) {
    dispatch({ type: 'FETCH_STOCKS_ERROR', error: error.message });
    }
  },
};

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case 'FETCH_STOCKS_START':
      return { ...state, isLoading: true, error: null };
    case 'RECEIVE_STOCKS':
      return { ...state, stocks: action.stocks, isLoading: false };
    case 'FETCH_STOCKS_ERROR':
      return { ...state, error: action.error, isLoading: false };
    default:
      return state;
  }
};

const selectors = {
  getStocks: (state) => state.stocks,
  getStocksError: (state) => state.error,
  isStocksLoading: (state) => state.isLoading,
};

const store = createReduxStore('my-investing-app/stocks', {
  reducer,
  actions,
  selectors,
});

register(store);

このプロセスでは、アクション、リデューサー、セレクタとともに、エラーと読み込み状態を含むデフォルトの状態を定義します。

ストアのデータを表示する

ストアを設定したら、その中の情報を表示するReactコンポーネントを作成します。

import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect } from '@wordpress/element';

const StockTicker = () => {
  const stocks = useSelect((select) => select('my-investing-app/stocks').getStocks());
  const error = useSelect((select) => select('my-investing-app/stocks').getStocksError());
  const isLoading = useSelect((select) => select('my-investing-app/stocks').isStocksLoading());

  const { fetchStocks } = useDispatch('my-investing-app/stocks');

  useEffect(() => {
    fetchStocks();
  }, []);

  if (isLoading) {
    return <p>株式データを読み込み中...</p>;
  }

  if (error) {
    return <p>エラー: {error}</p>;
  }

  return (
    <div className="stock-ticker">
      <h2>株式ティッカー</h2>
      <ul>
       {stocks.map((stock) => (
       <li key={stock.symbol}>
        {stock.symbol}: ${stock.price}
       </li>
       ))}
     </ul>
   </div>
  );
};

このコンポーネントは、データアクセス、アクションのディスパッチ、コンポーネントのライフサイクル管理を処理するために、useSelectuseDispatchフック(およびその他のフック)を使用します。また、独自のエラーおよび読み込み状態メッセージを設定し、ティッカーを実際に表示するためのコードも含まれています。この設定が完了したら、コンポーネントをWordPressに登録します。

WordPressにコンポーネントを登録する

WordPressに登録しないと、作成したコンポーネントを使うことができません。これは、Classic Themes用にデザインする場合はウィジェットになりますが、ブロックとして登録することを意味します。この例ではBlockを使用しています。

import { registerBlockType } from '@wordpress/blocks';
import { StockTicker } from './components/StockTicker';

registerBlockType('my-investing-app/stock-ticker', {
  title: '株式ティッカー',
  icon: 'chart-line',
  category: 'widgets',
  edit: StockTicker,
  save: () => null, // これは動的にレンダリング
});

このプロセスは、WordPress内でブロックを登録するための一般的なアプローチで、別な実装や設定は不要です。

状態の更新とユーザーインタラクションの管理

ブロックを登録したら、ユーザーとのやりとりやリアルタイムの更新を処理しなければなりません。これにはカスタムHTMLやJavaScriptに加えて、いくつかのインタラクティブな制御が必要です。

const StockControls = () => {
  const { addToWatchlist, removeFromWatchlist } = useDispatch('my-investing-app/stocks');
  return (
    <div className="stock-controls">
      <button onClick={() => addToWatchlist('AAPL')}>
        Add Apple to Watchlist
      </button>

      <button onClick={() => removeFromWatchlist('AAPL')}>
        ウォッチリストから削除
      </button>
    </div>
  );
};

リアルタイムの更新については、Reactコンポーネント内でインターバルを設定可能です。

useEffect(() => {
  const { updateStockPrice } = dispatch('my-investing-app/stocks');
  const interval = setInterval(() => {
    stocks.forEach(stock => {
      fetchStockPrice(stock.symbol)
        .then(price => updateStockPrice(stock.symbol, price));
    });
  }, 60000);

  return () => clearInterval(interval);
}, [stocks]);

この方法では、コンポーネントのデータをストアと同期させながら、明確な関係分離を維持することができます。@wordpress/dataがすべての状態更新を処理するため、アプリ内で一貫性が確保されます。

サーバーサイドレンダリング

最後に、サーバーサイドレンダリングを設定して、ページ読み込み時にストックデータが最新であることを保証します。これにはPHPの知識が必要になります。

function my_investing_app_render_stock_ticker($attributes, $content) {
  // APIから最新の株式データを取得
  $stocks = fetch_latest_stock_data();
  ob_start();
  ?>
  <div class="stock-ticker">
    <h2>株式ティッカー</h2>
    <ul>
      <?php foreach ($stocks as $stock) : ?>
        <li><?php echo esc_html($stock['symbol']); ?>: $<?php echo esc_html($stock['price']); ?></li>
      <?php endforeach; ?>
    </ul>
  </div>

  <?php
  return ob_get_clean();
}

register_block_type('my-investing-app/stock-ticker', array(
  'render_callback' => 'my_investing_app_render_stock_ticker'
));

これにより、データストアをWordPressに完全統合し、初期レンダリングからリアルタイムの更新、ユーザーとのインタラクションまでを処理することができます。

まとめ

WordPressのデータパッケージである@wordpress/dataは、複雑ではあるものの、アプリケーションの状態管理を行う堅牢なツールです。主な概念に加えて、関数、演算子、引数など、さまざまな要素がありますが、すべてのデータがグローバルストアに格納される必要はないということは覚えておいてください。ローカルコンポーネントの状態もコード内で重要な役割を持っています。

Steve Bonisteel Kinsta

Kinstaのテクニカルエディター。救急車や消防車を追いかける記者としてキャリアをスタート。1990年代後半からインターネット関連の技術情報を担当している。