削除ボタンの追加

前のパートでは、新しいページの作成機能を追加しました。このパートではアプリケーションに「削除」機能を追加します。

これから構築する機能は以下のようになります。

ステップ1: 「削除」ボタンの追加

まず DeletePageButton コンポーネントを作成し、PagesList コンポーネントのユーザーインターフェースを更新します。

import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';

const DeletePageButton = () => (
	<Button variant="primary">
		Delete
	</Button>
)

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>
					<td style={{width: 190}}>Actions</td>
				</tr>
			</thead>
			<tbody>
				{ pages?.map( ( page ) => (
					<tr key={page.id}>
						<td>{ decodeEntities( page.title.rendered ) }</td>
						<td>
							<div className="form-buttons">
								<PageEditButton pageId={ page.id } />
								{/*  以下が PagesList コンポーネント内で唯一の変更 */}
								<DeletePageButton pageId={ page.id }/>
							</div>
						</td>
					</tr>
				) ) }
			</tbody>
		</table>
	);
}

これで PagesList は以下のようになります。

ステップ2: ボタンと削除アクションの接続

Gutenberg のデータでは、WordPress REST APIから deleteEntityRecord アクションを使用してエンティティレコードを削除します。アクションはリクエストを送信し、結果を処理し、Redux ステート内にキャッシュされたデータを更新します。

以下はブラウザの開発ツール内でエンティティレコードの削除を試す方法です。

// deleteEntityRecord を呼び出すには有効なページ ID が必要。getEntityRecords を使用して最初の利用可能なページ ID を取得する。
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;

// そのページを削除する。
const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );

// API リクエストの成功または失敗により、promise は resolved または rejected を取得する。

REST API リクエストが終了すると、ページの一つがリストから消えていることが分かります。これは、そのリストが useSelect() フックと select( coreDataStore ).getEntityRecords( 'postType', 'page' ) セレクタによって生成されているためです。ベースとなるデータが変更されるたびに、リストは新しいデータで再レンダーされます。これはかなり便利です !

DeletePageButton がクリックされたときに、アクションをディスパッチします。

const DeletePageButton = ({ pageId }) => {
	const { deleteEntityRecord } = useDispatch( coreDataStore );
	const handleDelete = () => deleteEntityRecord( 'postType', 'page', pageId );
	return (
		<Button variant="primary" onClick={ handleDelete }>
			Delete
		</Button>
	);
}

ステップ3: 視覚的なフィードバックの追加

「Delete」ボタンをクリックした後、REST API リクエストが終了するまで少し時間がかかるかもしれません。このチュートリアルの以前のパートで実行したのと同様に、<Spinner /> コンポーネントでそれを伝えます。

これには isDeletingEntityRecord セレクタが必要です。このセレクタはパート3で紹介した isSavingEntityRecord セレクタに似ています。true または false を返しますが、決して HTTP リクエストは発行しません。

const DeletePageButton = ({ pageId }) => {
	// ...
	const { isDeleting } = useSelect(
		select => ({
			isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
		}),
		[ pageId ]
	)
	return (
		<Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
			{ isDeleting ? (
				<>
					<Spinner />
					Deleting...
				</>
			) : 'Delete' }
		</Button>
	);
}

実際の動作の様子です。

ステップ4: エラー処理

ここまでは楽観的に「削除」操作は常に成功すると仮定しました。しかし、残念ながら実際には REST API リクエストは、さまざまな理由で失敗します。

  • ウェブサイトはダウンする可能性がある。
  • 削除リクエストは正しくないかもしれない。
  • ページは処理中に誰かに削除されるかもしれない。

こうしたエラーが発生した際にユーザーに伝えるには、getLastEntityDeleteError セレクタを使用して、エラー情報を取り出す必要があります。

// 「9」を実際のページ ID で置換すること
wp.data.select( 'core' ).getLastEntityDeleteError( 'postType', 'page', 9 )

これを以下のように DeletePageButton に適用します。

import { useEffect } from 'react';
const DeletePageButton = ({ pageId }) => {
	// ...
	const { error, /* ... */ } = useSelect(
		select => ( {
			error: select( coreDataStore ).getLastEntityDeleteError( 'postType', 'page', pageId ),
			// ...
		} ),
		[pageId]
	);
	useEffect( () => {
		if ( error ) {
			// エラーの表示
		}
	}, [error] )

	// ...
}

error オブジェクトは @wordpress/api-fetch から来たもので、エラーに関する情報を含みます。以下のプロパティを持ちます。

  • message – Invalid post ID のような人間が読めるエラーメッセージ。
  • code – rest_post_invalid_id のような文字列ベースのエラーコード。すべてのエラーコードを調べるには、/v2/pages エンドポイントのソースコード を参照する必要があります。
  • data (オプション) – エラーの詳細。失敗したリクエストの HTTP レスポンスコードを含む code プロパティを含む。

このオブジェクトをエラーメッセージに変換する多くの方法がありますが、このチュートリアルでは error.message を表示します。

WordPress ではステータス情報を表示するパターンが確立されており、Snackbar コンポーネントを使用します。たとえば ウィジェットエディターでは、以下のようになります。

同じタイプの通知をプラグインで使用しましょう。これには2つのパートがあります。

  1. 通知の表示
  2. 通知のディスパッチ

通知の表示

アプリケーションはページを表示する方法のみを知っていて、通知を表示する方法は知りません。それを教えてあげましょう !

便利なことに WordPress では、通知のレンダーに必要なすべての React コンポーネントが提供されています。Snackbar コンポーネントは、単一の通知を表現します。

しかし、Snackbar は直接使わず、SnackbarList コンポーネントを使用します。SnackbarList コンポーネントはスムーズなアニメーションで複数の通知を表示し、数秒後に自動的に消えます。実際、WordPress ではウィジェットエディターやその他の管理画面のページで同じコンポーネントを使用しています。

それでは独自の Notifications コンポーネントを作成します。

import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';

function Notifications() {
	const notices = []; // すぐにここに戻ってきます !

	return (
		<SnackbarList
			notices={ notices }
			className="components-editor-notices__snackbar"
		/>
	);
}

基本的な構造はできていますが、レンダーする通知のリストが空です。どうすればいいのでしょうか ? WordPressと同じパッケージ @wordpress/notices に頼ってみましょう。

以下がその方法です。

import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';

function Notifications() {
	const notices = useSelect(
		( select ) => select( noticesStore ).getNotices(),
		[]
	);
	const { removeNotice } = useDispatch( noticesStore );
	const snackbarNotices = notices.filter( ({ type }) => type === 'snackbar' );

	return (
		<SnackbarList
			notices={ snackbarNotices }
			className="components-editor-notices__snackbar"
			onRemove={ removeNotice }
		/>
	);
}

function MyFirstApp() {
	// ...
	return (
		<div>
			{/* ... */}
			<Notifications />
		</div>
	);
}

このチュートリアルはページの管理に重点を置いているため、上のスニペットについては詳しく説明しません。もし @wordpress/notices の詳細に興味があれば、ハンドブックページ から始めると良いでしょう。

これで、発生した可能性のあるエラーをユーザーに伝える準備ができました。

通知のディスパッチ

SnackbarNotices コンポーネントを配置し、いくつかの通知をディスパッチする準備が整いました。以下がその方法です。

import { useEffect } from 'react';
import { store as noticesStore } from '@wordpress/notices';
function DeletePageButton( { pageId } ) {
	const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
	// コールバックの代わりにストアハンドルを渡すと、useSelect はセレクタのリストを返す:
	const { getLastEntityDeleteError } = useSelect( coreDataStore )
	const handleDelete = async () => {
		const success = await deleteEntityRecord( 'postType', 'page', pageId);
		if ( success ) {
			// 操作が成功したことをユーザーに伝える:
			createSuccessNotice( "The page was deleted!", {
				type: 'snackbar',
			} );
		} else {
			// deleteEntityRecord が失敗した*後で*、直接セレクタを使用して新しいエラーを取得する。
			const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
			const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
			// 具体的にどのように操作に失敗したかをユーザーに伝える:
			createErrorNotice( message, {
				type: 'snackbar',
			} );
		}
	}
	// ...
}

素晴らしい ! これで DeletePageButton は、完全にエラーを認識するようになりました。エラーメッセージを実際に見てみましょう。無効な削除をトリガーして失敗させます。これを行う1つの方法として、pageId に大きな数を掛けます。

function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
	pageId = pageId * 1000;
	// ...
}

ページを更新し、「Delete」ボタンをクリックすると、次のようなエラーメッセージが表示されるはずです。

最高です ! では、pageId = pageId * 1000; の行を削除しましょう。

そして、実際にページを削除してみます。ブラウザをリフレッシュして「Delete」ボタンをクリックすると、以下のように表示されます。

以上です。

すべてをひとつに

これですべてのピースがそろいました。この章で行ったすべての変更が以下になります。

import { useState, useEffect } from 'react';
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';

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

	return (
		<div>
			<div className="list-controls">
				<SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
				<PageCreateButton/>
			</div>
			<PagesList hasResolved={ hasResolved } pages={ pages }/>
			<Notifications />
		</div>
	);
}

function SnackbarNotices() {
	const notices = useSelect(
		( select ) => select( noticesStore ).getNotices(),
		[]
	);
	const { removeNotice } = useDispatch( noticesStore );
	const snackbarNotices = notices.filter( ( { type } ) => type === 'snackbar' );

	return (
		<SnackbarList
			notices={ snackbarNotices }
			className="components-editor-notices__snackbar"
			onRemove={ removeNotice }
		/>
	);
}

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>
					<td style={ { width: 190 } }>Actions</td>
				</tr>
			</thead>
			<tbody>
				{ pages?.map( ( page ) => (
					<tr key={ page.id }>
						<td>{ page.title.rendered }</td>
						<td>
							<div className="form-buttons">
								<PageEditButton pageId={ page.id }/>
								<DeletePageButton pageId={ page.id }/>
							</div>
						</td>
					</tr>
				) ) }
			</tbody>
		</table>
	);
}

function DeletePageButton( { pageId } ) {
	const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
	// コールバックの代わりにストアハンドルを渡すと、useSelect はセレクタのリストを返す:
	const { getLastEntityDeleteError } = useSelect( coreDataStore )
	const handleDelete = async () => {
		const success = await deleteEntityRecord( 'postType', 'page', pageId);
		if ( success ) {
			// 操作が成功したことをユーザーに伝える:
			createSuccessNotice( "The page was deleted!", {
				type: 'snackbar',
			} );
		} else {
			// この時点で、直接セレクタを使用してエラーを取得する。
			// 仮に、以下のようにエラーをフェッチしたとする。
			//     const { lastError } = useSelect( function() { /* ... */ } );
			// このとき lastError は handleDelete 内部で null になる。
			// 何故か ? それは handleDelete の呼び出しまえに計算されたバージョンを参照するため。
			const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
			const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
			// 具体的にどのように操作に失敗したかをユーザーに伝える:
			createErrorNotice( message, {
				type: 'snackbar',
			} );
		}
	}

	const { deleteEntityRecord } = useDispatch( coreDataStore );
	const { isDeleting } = useSelect(
		select => ( {
			isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
		} ),
		[ pageId ]
	);

	return (
		<Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
			{ isDeleting ? (
				<>
					<Spinner />
					Deleting...
				</>
			) : 'Delete' }
		</Button>
	);
}

次は ?

原文

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