WordPressのブロック開発メモ その9 データの操作(セレクタとアクション&ディスパッチ)

前回はフックについてでした。

WordPressのブロック開発メモ その8 フック(アクションとフィルター)

今回はRedux的なデータ操作について紹介します。

セレクタとアクション

状態を取得するセレクタと変更をするアクションがあります。
実行するにはそれぞれselect()dispatch()を使います。
Reduxの知識がある人は何となく想像つくと思いますが、
WordPressが独自に拡張したものなので使い方には違和感があるかもしれません。

これまで古いJavaScriptの書き方をしてきませんでしたが、
ブラウザ上で実行出来るメリットがあるので今回はそちらを使っていきます。

状態を取得するセレクタはwp.data.select()を、
状態を変更するアクションはwp.data.dispatch()を使います。

どちらも引数に名前空間を指定します。
早速使っていきます。

core/notices

まずselect()とdispatch()の両者を最も比較しやすい名前空間がcore/noticesでしょうか。

投稿ページでブラウザの開発者ツールを開き、
コンソールから以下を入力してエンターを押します。

wp.data.select('core/notices').getNotices()

すると通知一覧が表示されます。

でも実際には空の配列が。
これは当然でまだ通知が無いからです。

ではその通知ってなんやねん、ってことで通知を追加していきましょう。
以下をコンソールから入力して円ターキーを押します。

wp.data.dispatch('core/notices').createSuccessNotice('Kurage Success !!!');
wp.data.dispatch('core/notices').createWarningNotice('Kurage Warning...');
wp.data.dispatch('core/notices').createErrorNotice('Kurage Error !');
wp.data.dispatch('core/notices').createInfoNotice('Kurage Info.');

すると通知が4つ表示されます。

もう一度、getNotices()を使って一覧表示してみましょう。

wp.data.select('core/notices').getNotices()

今度は4つ分取得出来てます。

この通知は×ボタンをクリックすることで削除出来ます。
試しにInfoとWarningを削除してみましょう。
そしてもう一度

wp.data.select('core/notices').getNotices()

ErrorとSuccessの2つが取得出来ました。

今度はコードから削除してみましょう。
以下を入力・・・

wp.data.select('core/notices').getNotices().map(notice => {
    wp.data.dispatch('core/notices').removeNotice( notice.id );
});

残り二つも消えました。

  • removeNotice()の引数には通知のidを渡します。

このように、
getNotices()のような取得系はwp.data.select()を使い、
createSuccessNotice()やremoveNotice()のような更新系はwp.data.dispatch()を使います。

そしてその引数の名前空間(cpre/notices)は通知に関するデータを扱えます。
もちろんこれは例に挙げやすかったのでこの名前空間を使いましたが、他にも複数の名前空間が用意されてます。

core

coreという名前だけあってWordPressの中核的なデータを扱う名前空間です。
主にエンティティの取得操作、Undo/Redoなどが存在。

Undo/Redo

Undo/Redoとは元に戻すやり直すのことで、

セレクタには元に戻す、やり直すことがあるかどうかを論理値で返すhasUndo/hasRedo
それらの情報を取得するのgetUndoEdit/getRedoEditがあります。
アクションにはそれらを実行するundoredoがあります。

投稿をちゃちゃっと編集して実験してみましょう。

編集した直後、元に戻すはあってもやり直すは無いはずです。

wp.data.select('core').hasUndo()
true
wp.data.select('core').hasRedo()
false

ただし、undo()をディスパッチした場合にはやり直すが出現するはずです。

wp.data.dispatch('core').undo()
Promise {<fulfilled>: undefined}
wp.data.select('core').hasRedo()
true

ブロックパターン

ブロックパターンについては別途ググってください。

パターンカテゴリ

wp.data.select('core').getBlockPatternCategories();

パターン一覧

wp.data.select('core').getBlockPatterns();

エンティティレコード

REST API の結果を取得できます。

投稿タイプ一覧を取得

getEntitiesConfig(kind)

以下の例はpostTypeを指定した時です。

wp.data.select('core').getEntitiesConfig('postType');
{kind: 'postType', baseURL: '/wp/v2/posts', baseURLParams: {…}, name: 'post', label: '投稿', …}
{kind: 'postType', baseURL: '/wp/v2/pages', baseURLParams: {…}, name: 'page', label: '固定ページ', …}
{kind: 'postType', baseURL: '/wp/v2/media', baseURLParams: {…}, name: 'attachment', label: 'メディア', …}
{kind: 'postType', baseURL: '/wp/v2/menu-items', baseURLParams: {…}, name: 'nav_menu_item', label: 'ナビゲーションメニューの項目', …}
{kind: 'postType', baseURL: '/wp/v2/blocks', baseURLParams: {…}, name: 'wp_block', label: '再利用ブロック', …}
{kind: 'postType', baseURL: '/wp/v2/templates', baseURLParams: {…}, name: 'wp_template', label: 'テンプレート', …}
{kind: 'postType', baseURL: '/wp/v2/template-parts', baseURLParams: {…}, name: 'wp_template_part', label: 'テンプレートパーツ', …}
{kind: 'postType', baseURL: '/wp/v2/navigation', baseURLParams: {…}, name: 'wp_navigation', label: 'ナビゲーションメニュー', …}

エンティティ系の第一引数はkind(種類の意味)となってますが、これはpostTypetaxonomyがあるようです。
結果を見れば大体どんな情報が得れるか理解できます。

エンティティレコードを取得

getEntityRecord(kind, name, key)

以下の例は種類が投稿タイプで、投稿タイプの名前がpostで、主キーが335の場合です。

wp.data.select('core').getEntityRecord('postType', 'post', 335);

投稿のデータですね、内部では@wordpress/api-fetchを使ってREST APIにアクセスし投稿データを取得してます。

ちなみに最初に実行した時はundefinedが返って来ることに気づきましたか?
ブラウザを更新して、もう一度実行するとやっぱりundefinedが返ってきます。
実際に投稿データが返ってくるのはそれ以降実行した場合ですね。
このミステリーについては次回のリゾルバの項で解説します。

getEntityRecord()と似たgetRawEntityRecord()がありますがこちらはtitlecontentなどの結果が変わります。
Raw付の方は生のデータが入っているのに対し、Raw無しの方は生のデータとレンダリング後のデータが入ってます。

エディタ情報の取得

getEntityRecordEdits(kind, name, recordId)

以下の例は現在編集中の投稿IDが421の時です。

wp.data.select('core').getEntityRecordEdits('postType', 'post', 421);

他にもいろんなセレクタやアクションがありますが多すぎるのでこの辺にしておきます。
大まかな使い方は分かったと思います。

ブロックでの使い方

これをいつものブロックで使います。
useSelect()とuseDispatch()があります。

import React, { useState } from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';

import KurageExampleBlockProps from './props';
import './editor.scss';

import { useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { Button, TextControl } from '@wordpress/components';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{

    // @ts-ignore
    const notices = useSelect(select => select(noticesStore).getNotices());
    const { createSuccessNotice  } = useDispatch(noticesStore);

    const [message, setMessage] = useState('');

    const addNotice = () =>
    {
        // @ts-ignore
        createSuccessNotice(message);
    }

    return (
        <div {...useBlockProps()}>

            <h2>Notices</h2>
            {
                notices.map(_ => <p>{_.status} : {_.content}</p>)
            }

            <h2>Editor</h2>
            <div>
                <p>Message</p>
                <TextControl value={message} onChange={e => setMessage(e)} />
                <Button variant="primary" onClick={addNotice}>追加する</Button>
            </div>

        </div>
    );
}

テキストコントロールにそれぞれHello Kurage!, Kurage Attack!, Kurage vs Hitode!を入力してはボタンを押すを繰り返します。
通知が3つ表示されます。
ブロックには現在表示されている通知一覧を表示してます。

まず重要なのがストアの取得です。

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

この通知に関するデータは@wordpress/noticesにあるストアから取得することが出来ます。

これまでのようなセレクタの使い方をするにはuseSelect()を使います。

const notices = useSelect(select => select(noticesStore).getNotices());

コールバックの引数でselectを受け取り、そこにストアを渡します。
getNotices()から通知一覧を取得してます。

一方アクションの場合はuseDispatch()から取得します。

const { createSuccessNotice  } = useDispatch(noticesStore);

useDispatch()にストアを渡し関数を取得出来ます。
今回はcreateSuccessNotice()関数を取得しています。
ボタンをクリックした時、通知を追加する貯めに定義した関数addNotice()により使用されます。

さてこのストアなんですが、データ毎にストアが用意されています。
例えば名前空間がcoreなら@wordpress/core-dataから取得します。

import { store as coreStore } from '@wordpress/core-data';
import React, { useState } from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';

import KurageExampleBlockProps from './props';
import './editor.scss';

import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{

    // @ts-ignore
    const config = useSelect(select => select(coreStore).getEntitiesConfig('postType'));

    return (
        <div {...useBlockProps()}>

            <h2>Notices</h2>
            {
                config.map(_ => <p>{_.name} : {_.baseURL}</p>)
            }

        </div>
    );
}

コアのgetEntitiesConfigセレクタをブロックで描画しました。

やっていることはこれと一緒ですね。

wp.data.select('core').getEntitiesConfig('postType');

また型について文句言われる時は@ts-ignoreを使ったり、とりあえず以下の型定義をインストールするなどして対処します。

npm install --save-dev @types/wordpress__core-data
npm install --save-dev @types/wordpress__notices
npm install --save-dev @types/wordpress__data

高階コンポーネント

セレクタやアクションをコンポーネントから使えるようにする高階コンポーネントのwithSelect()withDispatch()についてです。

withSelect()

import React, { useState } from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';

import KurageExampleBlockProps from './props';
import './editor.scss';

import { store as noticesStore } from '@wordpress/notices';
import { withSelect } from '@wordpress/data';

const Edit = (props: BlockEditProps<KurageExampleBlockProps> & any) =>
{
    const { notices } = props;

    return (
        <div {...useBlockProps()}>

            <h2>Notices</h2>
            {
                notices.map(_ => <div>{_.status} : {_.content}</div>)
            }

        </div>
    );
}

// @ts-ignore
export default withSelect(select => {
    const notices = select(noticesStore).getNotices();
    return { notices };
})(Edit);

edit.tsxを変更しました。
これまでのコンポーネントをwithSelect()で拡張します。

export default withSelect(select => {
    const notices = select(noticesStore).getNotices();
    return { notices };
})(Edit);

引数からselectを取得出来ます。
戻り値でリテラルオブジェクトを返すと、そのプロパティをコンポーネントのpropsから取得出来るようになります。

const { notices } = props;

withDispatch()

次にwithDispatch()についてです。

import React from 'react';
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';

import KurageExampleBlockProps from './props';
import './editor.scss';

import { store as noticesStore } from '@wordpress/notices';
import { withSelect } from '@wordpress/data';
import { withDispatch } from '@wordpress/data';
import { Button, TextControl } from '@wordpress/components';
import { compose } from '@wordpress/compose';

const Edit = (props: BlockEditProps<KurageExampleBlockProps> & any) =>
{
    // propsから createSuccessNotice と createErrorNotice も取得出来る
    const { notices, createSuccessNotice, createErrorNotice } = props;

    const [success, setSuccess] = useState('');
    const [error, setError] = useState('');

    const onSuccess = () => createSuccessNotice(success);
    const onError = () => createErrorNotice(error);

    return (
        <div {...useBlockProps()}>

            <h2>Notices</h2>
            {
                notices.map(_ => <div>{_.status} : {_.content}</div>)
            }

            <div>
                Success:
                    <TextControl value={success} onChange={setSuccess} />
                    <Button variant="primary" onClick={onSuccess}>追加</Button>
            </div>
            <div>
                Error:
                    <TextControl value={error} onChange={setError} />
                    <Button variant="primary" onClick={onError}>追加</Button>
            </div>

        </div>
    );
}

// @ts-ignore
export default compose(

    withSelect(select => {
        const notices = select(noticesStore).getNotices();
        return { notices };
    }),

    withDispatch(dispatch => {
        const { createSuccessNotice, createErrorNotice } = dispatch(noticesStore);
        return { createSuccessNotice, createErrorNotice }
    })

)(Edit);

SuccessとErrorの通知を追加出来るようにしてます。
withDispatch()高階関数を使うことで引数からdispatchを取得出来ます。
リテラルオブジェクトを返すとそれをコンポーネントのpropsから取得出来るようになります。

withDispatch(dispatch => {
    const { createSuccessNotice, createErrorNotice } = dispatch(noticesStore);
    return { createSuccessNotice, createErrorNotice }
})

以下がpropsから取得している箇所です。

const { notices, createSuccessNotice, createErrorNotice } = props;

今回はwithSelect()withDispatch()を2つ使ってるので可読性を上げるためcompose()を使って合成してます。

次回はストアについてです。

WordPressのブロック開発メモ その10 Redux ストアの作成

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク