ステップ 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>
);
}
このコンポーネントはまだデータを取得せず、ハードコーディングされたページのリストのみを表示します。ページをリフレッシュすると、次のように表示されるはずです:
ステップ 2: データを取得する
ハードコーディングされたサンプルページはあまり役に立ちません。実際の WordPress ページを表示したいので、WordPress REST API から実際のページのリストを取得しましょう。
始める前に、実際に取得するページがあることを確認しましょう。WPAdmin 内で、サイドバーメニューを使用してページに移動し、少なくとも 4 つまたは 5 つのページが表示されていることを確認してください:
表示されない場合は、いくつかのページを作成してください – 上のスクリーンショットと同じタイトルを使用できます。必ず 公開 し、保存 するだけではないようにしてください。
データが用意できたので、コードに入っていきましょう。WordPress コア API と連携するためのリゾルバー、セレクター、アクションを提供する @wordpress/core-data
パッケージを活用します。@wordpress/core-data
は @wordpress/data
パッケージの上に構築されています。
ページのリストを取得するために、getEntityRecords
セレクターを使用します。大まかに言えば、正しい API リクエストを発行し、結果をキャッシュし、必要なレコードのリストを返します。使用方法は次のとおりです:
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )
ブラウザの開発者ツールでこのスニペットを実行すると、null
が返されるのがわかります。なぜですか?ページは最初に セレクター を実行した後に getEntityRecords
リゾルバーによってのみリクエストされます。少し待って再実行すると、すべてのページのリストが返されます。
注: このタイプのコマンドを直接実行するには、ブラウザがブロックエディターのインスタンスを表示していることを確認してください(任意のページで構いません)。そうでないと、select( 'core' )
関数は利用できず、エラーが発生します。
同様に、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' ),
[]
);
// ...
}
function PagesList({ pages }) {
// ...
<li key={page.id}>
{page.title.rendered}
</li>
// ...
}
index.js 内で import
ステートメントを使用していることに注意してください。これにより、プラグインは wp_enqueue_script
を使用して依存関係を自動的にロードできます。coreDataStore
への参照は、ブラウザの開発者ツールで使用する同じ wp.data
参照にコンパイルされます。
useSelect
は 2 つの引数を取ります: コールバックと依存関係。大まかに言えば、依存関係または基盤となるデータストアが変更されるたびにコールバックを再実行します。[https://developer.wordpress.org/block-editor/reference-guide/packages/packages-data/#useselect] で [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>
)
}
投稿タイトルには á
のような HTML エンティティが含まれる場合があるため、decodeEntities
関数を使用して、それらを á
のようなシンボルに置き換える必要があります。
ページをリフレッシュすると、次のようなリストが表示されるはずです:
ステップ 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>
);
}
ステップ 4: 検索ボックスを追加する
ページのリストは今のところ短いですが、成長するにつれて扱いが難しくなります。WordPress の管理者は通常、この問題を検索ボックスで解決します – 私たちも実装しましょう!
まず、検索フィールドを追加します:
import { useState } from 'react';
import { SearchControl } from '@wordpress/components';
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?search=home
へのリクエストがトリガーされ、/wp/v2/pages
だけではなくなります。
これを 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
依存関係のリスト内にも指定されており、getEntityRecords
が searchTerm
の変更時に再実行されることを確認します。
最後に、すべてを組み合わせた MyFirstApp
の見た目は次のようになります:
import { useState } from 'react';
import { createRoot } from 'react-dom';
import { SearchControl } from '@wordpress/components';
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>
)
}
これで!結果をフィルタリングできるようになりました:
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」を検索すると、A
、Ab
、Abo
、Abou
、About
をフィルタリングする 5 つの API リクエストがトリガーされます。これらのリクエストは、開始した順序とは異なる順序で完了する可能性があります。search=A が search=About の後に解決される可能性があり、そのために間違ったデータを表示することになります。
Gutenberg データは、背後で非同期部分を処理することで助けます。useSelect
は最新の呼び出しを記憶し、期待されるデータのみを返します。
次に、各キーストロークが API リクエストをトリガーします。About
を入力し、それを削除して再入力すると、データを再利用できるにもかかわらず、合計 10 のリクエストが発行されます。
Gutenberg データは、getEntityRecords()
によってトリガーされた API リクエストの応答をキャッシュし、次回の呼び出しで再利用します。これは、他のコンポーネントが同じエンティティレコードに依存している場合に特に重要です。
全体として、core-data に組み込まれたユーティリティは、典型的な問題を解決するように設計されているため、アプリケーションに集中できます。
ステップ 5: ローディングインジケーター
検索機能には 1 つの問題があります。まだ検索中か、結果がないのかはっきりしません:
Loading… や No results のようなメッセージがあれば、状況が明確になります。これを実装しましょう!まず、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] );
// ...
}
最後の問題が 1 つあります。タイプミスをして getEntityRecords
と hasFinishedResolution
に異なる引数を渡すことは簡単です。それらが同一であることが重要です。このリスクを取り除くために、引数を変数に格納します:
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] );
// ...
}
すべてを結びつける
すべての要素が揃いました、素晴らしい!アプリの完全な JavaScript コードは次のとおりです:
import { useState } from 'react';
import { createRoot } from 'react-dom';
import { SearchControl, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
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>
);
}
const root = createRoot(
document.querySelector( '#my-first-gutenberg-app' )
);
window.addEventListener(
'load',
function () {
root.render(
<MyFirstApp />
);
},
false
);
残るはページをリフレッシュして、新しいステータスインジケーターを楽しむだけです: