ステップ 1: PagesList コンポーネントを構築する

最初に、ページのリストを表示する最小限の React コンポーネントを構築しましょう:

  1. function MyFirstApp() {
  2. const pages = [{ id: 'mock', title: 'Sample page' }]
  3. return <PagesList pages={ pages }/>;
  4. }
  5. function PagesList( { pages } ) {
  6. return (
  7. <ul>
  8. { pages?.map( page => (
  9. <li key={ page.id }>
  10. { page.title }
  11. </li>
  12. ) ) }
  13. </ul>
  14. );
  15. }

このコンポーネントはまだデータを取得せず、ハードコーディングされたページのリストのみを表示します。ページをリフレッシュすると、次のように表示されるはずです:

ページのリストを作成する(Building a list of pages) - img1

ステップ 2: データを取得する

ハードコーディングされたサンプルページはあまり役に立ちません。実際の WordPress ページを表示したいので、WordPress REST API から実際のページのリストを取得しましょう。

始める前に、実際に取得するページがあることを確認しましょう。WPAdmin 内で、サイドバーメニューを使用してページに移動し、少なくとも 4 つまたは 5 つのページが表示されていることを確認してください:

ページのリストを作成する(Building a list of pages) - img2

表示されない場合は、いくつかのページを作成してください – 上のスクリーンショットと同じタイトルを使用できます。必ず 公開 し、保存 するだけではないようにしてください。

データが用意できたので、コードに入っていきましょう。WordPress コア API と連携するためのリゾルバー、セレクター、アクションを提供する @wordpress/core-data パッケージを活用します。@wordpress/core-data@wordpress/data パッケージの上に構築されています。

ページのリストを取得するために、getEntityRecords セレクターを使用します。大まかに言えば、正しい API リクエストを発行し、結果をキャッシュし、必要なレコードのリストを返します。使用方法は次のとおりです:

  1. wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )

ブラウザの開発者ツールでこのスニペットを実行すると、null が返されるのがわかります。なぜですか?ページは最初に セレクター を実行した後に getEntityRecords リゾルバーによってのみリクエストされます。少し待って再実行すると、すべてのページのリストが返されます。

注: このタイプのコマンドを直接実行するには、ブラウザがブロックエディターのインスタンスを表示していることを確認してください(任意のページで構いません)。そうでないと、select( 'core' ) 関数は利用できず、エラーが発生します。

同様に、MyFirstApp コンポーネントはデータが利用可能になるとセレクターを再実行する必要があります。これがまさに useSelect フックの役割です:

  1. import { useSelect } from '@wordpress/data';
  2. import { store as coreDataStore } from '@wordpress/core-data';
  3. function MyFirstApp() {
  4. const pages = useSelect(
  5. select =>
  6. select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
  7. []
  8. );
  9. // ...
  10. }
  11. function PagesList({ pages }) {
  12. // ...
  13. <li key={page.id}>
  14. {page.title.rendered}
  15. </li>
  16. // ...
  17. }

index.js 内で import ステートメントを使用していることに注意してください。これにより、プラグインは wp_enqueue_script を使用して依存関係を自動的にロードできます。coreDataStore への参照は、ブラウザの開発者ツールで使用する同じ wp.data 参照にコンパイルされます。

useSelect は 2 つの引数を取ります: コールバックと依存関係。大まかに言えば、依存関係または基盤となるデータストアが変更されるたびにコールバックを再実行します。[https://developer.wordpress.org/block-editor/reference-guide/packages/packages-data/#useselect] で [useSelect] について詳しく学ぶことができます。

まとめると、次のコードが得られます:

  1. import { useSelect } from '@wordpress/data';
  2. import { store as coreDataStore } from '@wordpress/core-data';
  3. import { decodeEntities } from '@wordpress/html-entities';
  4. function MyFirstApp() {
  5. const pages = useSelect(
  6. select =>
  7. select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
  8. []
  9. );
  10. return <PagesList pages={ pages }/>;
  11. }
  12. function PagesList( { pages } ) {
  13. return (
  14. <ul>
  15. { pages?.map( page => (
  16. <li key={ page.id }>
  17. { decodeEntities( page.title.rendered ) }
  18. </li>
  19. ) ) }
  20. </ul>
  21. )
  22. }

投稿タイトルには &aacute; のような HTML エンティティが含まれる場合があるため、decodeEntities 関数を使用して、それらを á のようなシンボルに置き換える必要があります。

ページをリフレッシュすると、次のようなリストが表示されるはずです:

ページのリストを作成する(Building a list of pages) - img3

ステップ 3: テーブルに変換する

  1. function PagesList( { pages } ) {
  2. return (
  3. <table className="wp-list-table widefat fixed striped table-view-list">
  4. <thead>
  5. <tr>
  6. <th>Title</th>
  7. </tr>
  8. </thead>
  9. <tbody>
  10. { pages?.map( page => (
  11. <tr key={ page.id }>
  12. <td>{ decodeEntities( page.title.rendered ) }</td>
  13. </tr>
  14. ) ) }
  15. </tbody>
  16. </table>
  17. );
  18. }

ページのリストを作成する(Building a list of pages) - img4

ステップ 4: 検索ボックスを追加する

ページのリストは今のところ短いですが、成長するにつれて扱いが難しくなります。WordPress の管理者は通常、この問題を検索ボックスで解決します – 私たちも実装しましょう!

まず、検索フィールドを追加します:

  1. import { useState } from 'react';
  2. import { SearchControl } from '@wordpress/components';
  3. function MyFirstApp() {
  4. const [searchTerm, setSearchTerm] = useState( '' );
  5. // ...
  6. return (
  7. <div>
  8. <SearchControl
  9. onChange={ setSearchTerm }
  10. value={ searchTerm }
  11. />
  12. {/* ... */ }
  13. </div>
  14. )
  15. }

input タグを使用する代わりに、SearchControl コンポーネントを活用しました。これがその見た目です:

ページのリストを作成する(Building a list of pages) - img5

フィールドは空の状態で始まり、内容は searchTerm ステート値に保存されます。useState フックに不慣れな場合は、React のドキュメント で詳しく学ぶことができます。

これで、searchTerm に一致するページのみをリクエストできます。

WordPress API ドキュメント を確認したところ、/wp/v2/pages エンドポイントは search クエリパラメータを受け入れ、それを使用して 文字列に一致する結果を制限します。では、どのように使用できますか?次のように、getEntityRecords にカスタムクエリパラメータを第 3 引数として渡すことができます:

  1. wp.data.select( 'core' ).getEntityRecords( 'postType', 'page', { search: 'home' } )

ブラウザの開発者ツールでそのスニペットを実行すると、/wp/v2/pages?search=home へのリクエストがトリガーされ、/wp/v2/pages だけではなくなります。

これを useSelect 呼び出しに反映させましょう:

  1. import { useSelect } from '@wordpress/data';
  2. import { store as coreDataStore } from '@wordpress/core-data';
  3. function MyFirstApp() {
  4. // ...
  5. const { pages } = useSelect( select => {
  6. const query = {};
  7. if ( searchTerm ) {
  8. query.search = searchTerm;
  9. }
  10. return {
  11. pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query )
  12. }
  13. }, [searchTerm] );
  14. // ...
  15. }

searchTerm は、提供された場合、search クエリパラメータとして使用されます。searchTerm は、useSelect 依存関係のリスト内にも指定されており、getEntityRecordssearchTerm の変更時に再実行されることを確認します。

最後に、すべてを組み合わせた MyFirstApp の見た目は次のようになります:

  1. import { useState } from 'react';
  2. import { createRoot } from 'react-dom';
  3. import { SearchControl } from '@wordpress/components';
  4. import { useSelect } from '@wordpress/data';
  5. import { store as coreDataStore } from '@wordpress/core-data';
  6. function MyFirstApp() {
  7. const [searchTerm, setSearchTerm] = useState( '' );
  8. const pages = useSelect( select => {
  9. const query = {};
  10. if ( searchTerm ) {
  11. query.search = searchTerm;
  12. }
  13. return select( coreDataStore ).getEntityRecords( 'postType', 'page', query );
  14. }, [searchTerm] );
  15. return (
  16. <div>
  17. <SearchControl
  18. onChange={ setSearchTerm }
  19. value={ searchTerm }
  20. />
  21. <PagesList pages={ pages }/>
  22. </div>
  23. )
  24. }

これで!結果をフィルタリングできるようになりました:

ページのリストを作成する(Building a list of pages) - img6

core-data を使用する代わりに API を直接呼び出す

少し立ち止まって、私たちが取ることができた別のアプローチの欠点を考えてみましょう – API を直接操作することです。API リクエストを直接送信したと想像してみてください:

  1. import apiFetch from '@wordpress/api-fetch';
  2. function MyFirstApp() {
  3. // ...
  4. const [pages, setPages] = useState( [] );
  5. useEffect( () => {
  6. const url = '/wp-json/wp/v2/pages?search=' + searchTerm;
  7. apiFetch( { url } )
  8. .then( setPages )
  9. }, [searchTerm] );
  10. // ...
  11. }

core-data の外で作業する場合、ここで 2 つの問題を解決する必要があります。

まず、順序が乱れた更新です。「About」を検索すると、AAbAboAbouAbout をフィルタリングする 5 つの API リクエストがトリガーされます。これらのリクエストは、開始した順序とは異なる順序で完了する可能性があります。search=Asearch=About の後に解決される可能性があり、そのために間違ったデータを表示することになります。

Gutenberg データは、背後で非同期部分を処理することで助けます。useSelect は最新の呼び出しを記憶し、期待されるデータのみを返します。

次に、各キーストロークが API リクエストをトリガーします。About を入力し、それを削除して再入力すると、データを再利用できるにもかかわらず、合計 10 のリクエストが発行されます。

Gutenberg データは、getEntityRecords() によってトリガーされた API リクエストの応答をキャッシュし、次回の呼び出しで再利用します。これは、他のコンポーネントが同じエンティティレコードに依存している場合に特に重要です。

全体として、core-data に組み込まれたユーティリティは、典型的な問題を解決するように設計されているため、アプリケーションに集中できます。

ステップ 5: ローディングインジケーター

検索機能には 1 つの問題があります。まだ検索中か、結果がないのかはっきりしません:

ページのリストを作成する(Building a list of pages) - img7

Loading…No results のようなメッセージがあれば、状況が明確になります。これを実装しましょう!まず、PagesList は現在のステータスを認識する必要があります:

  1. import { SearchControl, Spinner } from '@wordpress/components';
  2. function PagesList( { hasResolved, pages } ) {
  3. if ( !hasResolved ) {
  4. return <Spinner/>
  5. }
  6. if ( !pages?.length ) {
  7. return <div>No results</div>
  8. }
  9. // ...
  10. }
  11. function MyFirstApp() {
  12. // ...
  13. return (
  14. <div>
  15. // ...
  16. <PagesList hasResolved={ hasResolved } pages={ pages }/>
  17. </div>
  18. )
  19. }

カスタムローディングインジケーターを構築する代わりに、Spinner コンポーネントを活用しました。

ページセレクター hasResolved の状態を知る必要があります。hasFinishedResolution セレクターを使用して確認できます:

wp.data.select('core').hasFinishedResolution( 'getEntityRecords', [ 'postType', 'page', { search: 'home' } ] )

セレクターの名前と そのセレクターに渡したのと全く同じ引数 を取り、データがすでに読み込まれている場合は true を、まだ待機中の場合は false を返します。これを useSelect に追加しましょう:

  1. import { useSelect } from '@wordpress/data';
  2. import { store as coreDataStore } from '@wordpress/core-data';
  3. function MyFirstApp() {
  4. // ...
  5. const { pages, hasResolved } = useSelect( select => {
  6. // ...
  7. return {
  8. pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query ),
  9. hasResolved:
  10. select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', ['postType', 'page', query] ),
  11. }
  12. }, [searchTerm] );
  13. // ...
  14. }

最後の問題が 1 つあります。タイプミスをして getEntityRecordshasFinishedResolution に異なる引数を渡すことは簡単です。それらが同一であることが重要です。このリスクを取り除くために、引数を変数に格納します:

  1. import { useSelect } from '@wordpress/data';
  2. import { store as coreDataStore } from '@wordpress/core-data';
  3. function MyFirstApp() {
  4. // ...
  5. const { pages, hasResolved } = useSelect( select => {
  6. // ...
  7. const selectorArgs = [ 'postType', 'page', query ];
  8. return {
  9. pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
  10. hasResolved:
  11. select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', selectorArgs ),
  12. }
  13. }, [searchTerm] );
  14. // ...
  15. }

そして、これで完了です!

すべてを結びつける

すべての要素が揃いました、素晴らしい!アプリの完全な JavaScript コードは次のとおりです:

  1. import { useState } from 'react';
  2. import { createRoot } from 'react-dom';
  3. import { SearchControl, Spinner } from '@wordpress/components';
  4. import { useSelect } from '@wordpress/data';
  5. import { store as coreDataStore } from '@wordpress/core-data';
  6. import { decodeEntities } from '@wordpress/html-entities';
  7. function MyFirstApp() {
  8. const [ searchTerm, setSearchTerm ] = useState( '' );
  9. const { pages, hasResolved } = useSelect(
  10. ( select ) => {
  11. const query = {};
  12. if ( searchTerm ) {
  13. query.search = searchTerm;
  14. }
  15. const selectorArgs = [ 'postType', 'page', query ];
  16. return {
  17. pages: select( coreDataStore ).getEntityRecords(
  18. ...selectorArgs
  19. ),
  20. hasResolved: select( coreDataStore ).hasFinishedResolution(
  21. 'getEntityRecords',
  22. selectorArgs
  23. ),
  24. };
  25. },
  26. [ searchTerm ]
  27. );
  28. return (
  29. <div>
  30. <SearchControl onChange={ setSearchTerm } value={ searchTerm } />
  31. <PagesList hasResolved={ hasResolved } pages={ pages } />
  32. </div>
  33. );
  34. }
  35. function PagesList( { hasResolved, pages } ) {
  36. if ( ! hasResolved ) {
  37. return <Spinner />;
  38. }
  39. if ( ! pages?.length ) {
  40. return <div>No results</div>;
  41. }
  42. return (
  43. <table className="wp-list-table widefat fixed striped table-view-list">
  44. <thead>
  45. <tr>
  46. <td>Title</td>
  47. </tr>
  48. </thead>
  49. <tbody>
  50. { pages?.map( ( page ) => (
  51. <tr key={ page.id }>
  52. <td>{ decodeEntities( page.title.rendered ) }</td>
  53. </tr>
  54. ) ) }
  55. </tbody>
  56. </table>
  57. );
  58. }
  59. const root = createRoot(
  60. document.querySelector( '#my-first-gutenberg-app' )
  61. );
  62. window.addEventListener(
  63. 'load',
  64. function () {
  65. root.render(
  66. <MyFirstApp />
  67. );
  68. },
  69. false
  70. );

残るはページをリフレッシュして、新しいステータスインジケーターを楽しむだけです:

ページのリストを作成する(Building a list of pages) - img8

ページのリストを作成する(Building a list of pages) - img9

次は何ですか?