ページリストの構築

このパートでは、WordPress のすべての固定ページの、フィルタリング可能なリストを構築します。このセクションを終えると、以下のようなアプリが完成します。

手順を一つずつ見ていきましょう。

ステップ 1: PagesList コンポーネントの構築

まず、ページのリストを表示する最小限の React コンポーネントを作ります。

function MyFirstApp() {
	const pages = [{ id: 'mock', title: 'Sample page' }]
	return <PagesList pages={ pages }/>;
}

function PagesList( { pages } ) {
	return (
		<ul>
			{ pages?.map( page => (
				<li key={ page.id }>
					{ page.title }
				</li>
			) ) }
		</ul>
	);
}

注意: このコンポーネントはまだデータを取得しておらず、ハードコードされたページのリストを表示します。ページを更新すると、以下のように表示されます。

Top ↑

ステップ 2: データの取得

ハードコードされたサンプルページでは、何の役にも立ちません。実際の WordPress のページを表示するには、WordPress REST API から実際のページのリストを取得します。

その前に、実際に取得するページがあることを確認しましょう。管理画面のサイドバーメニューから「ページ」に移動します。 少なくとも4~5ページはあることを確認します。

固定ページがなければ、何ページか作成してください。上のスクリーンショットと同じタイトルを使用できます。このとき保存するだけでなく、必ず公開してください。

データが揃ったところで、コードを見ていきましょう。ここでは、@wordpress/core-data パッケージ を利用します。このパッケージは、WordPress のコア API を操作するリゾルバ、セレクタ、アクションを提供します。WordPress/core-data は、@wordpress/data パッケージの上に構築されています。

ページのリストの取得には、getEntityRecords セレクタを使用します。このセレクタは主に、正しい API リクエストを発行し、結果をキャッシュし、必要なレコードのリストを返します。以下のように使用します。

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

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

同様に、MyFirstApp コンポーネントも、データが利用可能になった時点でセレクタを再実行する必要があります。まさに useSelect フックはこれを実行しています。

import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';

function MyFirstApp() {
	const pages = useSelect(
		select =>
			select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
		[]
	);
	// ...
}

注意: index.js 内で import ステートメントを使用しています。これにより、プラグインは wp_enqueue_script を使用して自動的に依存関係を読み込めます。coreDataStore へのすべての参照は、ブラウザの devtools で使用しているのと同じ wp.data への参照にコンパイルされます。

useSelect は、コールバックと依存関係の2つの引数を取ります。依存関係、または基礎となるデータストアが変更された場合はいつでも、コールバックを再実行します。詳細についてはデータモジュールのドキュメントの useSelect を参照してください。

これまでのコードをまとめると、以下のようになります。

import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';

function MyFirstApp() {
	const pages = useSelect(
		select =>
			select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
		[]
	);
	return <PagesList pages={ pages }/>;
}

function PagesList( { pages } ) {
	return (
		<ul>
			{ pages?.map( page => (
				<li key={ page.id }>
					{ decodeEntities( page.title.rendered ) }
				</li>
			) ) }
		</ul>
	)
}

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

ページをリフレッシュすると、以下のようにリストが表示されます。

Top ↑

ステップ 3: テーブルの中へ

function PagesList( { pages } ) {
	return (
		<table className="wp-list-table widefat fixed striped table-view-list">
			<thead>
				<tr>
					<th>Title</th>
				</tr>
			</thead>
			<tbody>
				{ pages?.map( page => (
					<tr key={ page.id }>
						<td>{ decodeEntities( page.title.rendered ) }</td>
					</tr>
				) ) }
			</tbody>
		</table>
	);
}

Top ↑

ステップ 4: 検索ボックスの追加

ページのリストは今のところ短いですが、長くなればなるほど、作業が難しくなります。WordPress の管理では通常、この種の問題を検索ボックスで解決します。私たちも検索ボックスを実装してみましょう。

検索フィールドを追加するところから始めます。

import { SearchControl } from '@wordpress/components';
import { useState, render } from '@wordpress/element';

function MyFirstApp() {
	const [searchTerm, setSearchTerm] = useState( '' );
	// ...
	return (
		<div>
			<SearchControl
				onChange={ setSearchTerm }
				value={ searchTerm }
			/>
			{/* ... */ }
		</div>
	)
}

注意: inputタグを使用する代わりに、SearchControl コンポーネントを利用します。

フィールドは空で始まり、内容は searchTerm ステート値に格納されます。useState フックがよく分からない場合は、Reactのドキュメントを参照してください。

これで searchTerm にマッチするページのみをリクエストできます。

WordPress API ドキュメント で確認すると、wp/v2/pages エンドポイントが search クエリパラメータを受け入れ、文字列にマッチしたものに結果を限定する ことがわかります。しかし、どのように使えばよいでしょう ? それには、カスタムクエリパラメータを、getEntityRecords の第3引数として指定します。

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

ブラウザの開発ツールでこのスニペットを実行すると、/wp/v2/pages の代わりに、/wp/v2/pages?search=home へのリクエストが発生します。

useSelect 呼び出しでもこれを真似します。

import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';

function MyFirstApp() {
	// ...
	const { pages } = useSelect( select => {
		const query = {};
		if ( searchTerm ) {
			query.search = searchTerm;
		}
		return {
			pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query )
		}
	}, [searchTerm] );

	// ...
}

searchTerm が指定された場合、search クエリパラメータとして使用されます。注意: searchTerm はまた、useSelect の依存関係のリスト内でも使用されます。これは、searchTerm が変更された際に、getEntityRecords を再実行するためです。

最後に、全部をつなぐと、MyFirstApp はこうなります。

import { SearchControl } from '@wordpress/components';
import { useState, render } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';

function MyFirstApp() {
	const [searchTerm, setSearchTerm] = useState( '' );
	const pages = useSelect( select => {
		const query = {};
		if ( searchTerm ) {
			query.search = searchTerm;
		}
		return select( coreDataStore ).getEntityRecords( 'postType', 'page', query );
	}, [searchTerm] );

	return (
		<div>
			<SearchControl
				onChange={ setSearchTerm }
				value={ searchTerm }
			/>
			<PagesList pages={ pages }/>
		</div>
	)
}

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

Top ↑

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

ここで少し小休止して、API を直接操作した場合のデメリットについて考えてみます。API リクエストを直接送ったと想像してください。

import { apiFetch } from '@wordpress/api-fetch';
function MyFirstApp() {
	// ...
	const [pages, setPages] = useState( [] );
	useEffect( () => {
		const url = '/wp-json/wp/v2/pages?search=' + searchTerm;
		apiFetch( { url } )
			.then( setPages )
	}, [searchTerm] );
	// ...
}

core-data を使用せずに作業すると、2つの問題を解決する必要があります。

まず、ランダムな更新の問題です。「About」を検索すると、AAbAboAbouAbout をフィルタリングする、5つのAPIリクエストが発生します。このリクエストは、呼び出しと異なる順番で終了する可能性があります。つまり、search=About の後に search=A が解決される可能性があり、誤ったデータが表示されます。

Gutenberg Data は、裏で非同期部分を処理します。useSelect は直近の呼び出しを記憶しており、期待するデータのみを返します。

次に、キー操作のたびにAPIリクエストが発生する問題です。例えば、About と入力した後に、それを削除し、再度入力した場合、データを再利用できるにもかかわらず、合計で10回のリクエストが発生します。

Gutenberg Data は、getEntityRecords() をトリガーとする API リクエストのレスポンスをキャッシュして、以降の呼び出しで再利用します。この動きは特に、他のコンポーネントが同じエンティティレコードに依存している場合に重要です。

つまり、core-data に組み込まれたユーティリティが、一般的な問題を解決するよう設計されているため、開発者はアプリケーションの開発に集中できます。

Top ↑

ステップ 5: インジケータのロード

この検索機能には1つ問題があります。まだ検索中なのか、検索結果が表示されていないのかが、はっきりしないのです。

これは、「ロード中」や「結果がありません」のようなメッセージを出せばクリアになります。早速、実装しましょう。まず、 PagesList は現在の状態を認識する必要があります。

import { SearchControl, Spinner } from '@wordpress/components';
function PagesList( { hasResolved, pages } ) {
	if ( !hasResolved ) {
		return <Spinner/>
	}
	if ( !pages?.length ) {
		return <div>No results</div>
	}
	// ...
}

function MyFirstApp() {
	// ...

	return (
		<div>
			// ...
			<PagesList hasResolved={ hasResolved } pages={ pages }/>
		</div>
	)
}

注意: 独自のローディングインジケータを作成する代わりに、Spinner コンポーネントを活用しています。

ページセレクタが hasResolved かどうかを知る必要があります。これには、hasFinishedResolution セレクタを使用します。

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

これはセレクタの名前と、そのセレクタに渡したものと全く同じ引数 を受け取り、データがすでにロードされていれば true を、まだ待っている場合は false を返します。これを useSelect に追加します。

import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';

function MyFirstApp() {
	// ...
	const { pages, hasResolved } = useSelect( select => {
		// ...
		return {
			pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query ),
			hasResolved:
				select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', ['postType', 'page', query] ),
		}
	}, [searchTerm] );

	// ...
}

最後にもう一つだけ問題があります。タイプミスをして、 getEntityRecords と hasFinishedResolution に、異なる引数を渡してしまう場合があります。2つの引数が同じであることは非常に重要です。引数を変数に格納すれば、このリスクを取り除けます。

import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
	// ...
	const { pages, hasResolved } = useSelect( select => {
		// ...
		const selectorArgs = [ 'postType', 'page', query ];
		return {
			pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
			hasResolved:
				select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', selectorArgs ),
		}
	}, [searchTerm] );

	// ...
}

これで、完成です。

Top ↑

すべてをひとつに

すべての部品が揃いました、素晴らしい。以下は、アプリの完全なJavaScriptコードです。

import { SearchControl, Spinner } from '@wordpress/components';
import { useState, render } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';

function MyFirstApp() {
	const [ searchTerm, setSearchTerm ] = useState( '' );
	const { pages, hasResolved } = useSelect(
		( select ) => {
			const query = {};
			if ( searchTerm ) {
				query.search = searchTerm;
			}
			const selectorArgs = [ 'postType', 'page', query ];
			return {
				pages: select( coreDataStore ).getEntityRecords(
					...selectorArgs
				),
				hasResolved: select( coreDataStore ).hasFinishedResolution(
					'getEntityRecords',
					selectorArgs
				),
			};
		},
		[ searchTerm ]
	);

	return (
		<div>
			<SearchControl onChange={ setSearchTerm } value={ searchTerm } />
			<PagesList hasResolved={ hasResolved } pages={ pages } />
		</div>
	);
}

function PagesList( { hasResolved, pages } ) {
	if ( ! hasResolved ) {
		return <Spinner />;
	}
	if ( ! pages?.length ) {
		return <div>No results</div>;
	}

	return (
		<table className="wp-list-table widefat fixed striped table-view-list">
			<thead>
				<tr>
					<td>Title</td>
				</tr>
			</thead>
			<tbody>
				{ pages?.map( ( page ) => (
					<tr key={ page.id }>
						<td>{ decodeEntities( page.title.rendered ) }</td>
					</tr>
				) ) }
			</tbody>
		</table>
	);
}

window.addEventListener(
	'load',
	function () {
		render(
			<MyFirstApp />,
			document.querySelector( '#my-first-gutenberg-app' )
		);
	},
	false
);

あとは、ページを更新して、生まれたてのステータス表示を見て楽しんでください。

Top ↑

次のステップ

原文

最終更新日: