1. ホーム
  2. WordPress

【WordPress】createNotice の使い方|ブロックエディターに通知メッセージを表示する方法

Share

カスタムブロックの中で API を呼んだり、入力内容を検証したりしたとき、その結果をユーザーにどう伝えていますか。alert() を使うと編集の手が止まってしまいますし、見た目もエディターから浮いてしまいます。ブロックエディターには通知を表示するための仕組みが用意されていて、core/notices データストアの createNotice を呼ぶだけで、投稿を公開したときと同じ見た目の通知を出せます。この記事では createNotice の引数とオプション、createSuccessNotice などのショートハンド、通知を消す removeNotice の使い方を、実際のブロックのコードとあわせて解説します。

alert() ではなくエディターの通知を使う理由

ブロックエディターには、もともと2種類の通知の置き場所があります。ひとつはエディター上部に出るバナーで、「更新に失敗しました。」のようなメッセージが出るあの帯です。もうひとつがスナックバーで、下書きを保存したときに画面の下のほうへ小さく現れ、数秒で自動的に消えるあの通知です。

この2つはどちらも core/notices というデータストアが管理しています。自作のブロックやプラグインからこのストアにメッセージを追加すれば、WordPress 本体が出す通知とまったく同じ場所・同じ見た目で表示されます。スタイルを自分で用意する必要はありませんし、スクリーンリーダーへの読み上げも WordPress 側が面倒を見てくれます。

alert() や confirm() はブラウザーの処理を止めてしまうため、編集中に何度も出ると作業の邪魔になります。window.confirm() のように「操作を確実に止めたい」場面を除けば、結果の報告はエディターの通知に任せるほうが自然です。

createNotice の基本の使い方

通知はデータストアへの「アクション」として送ります。React のコンポーネントの中からは @wordpress/data の useDispatch を使い、@wordpress/notices が公開しているストア定義を渡してアクションを取り出します。

src/my-block/edit.js
import { useBlockProps } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

export default function Edit() {
    const blockProps = useBlockProps();

    // notices ストアから createNotice を取り出す
    const { createNotice } = useDispatch( noticesStore );

    return (
        <div { ...blockProps }>
            <Button
                variant="primary"
                onClick={ () =>
                    createNotice( 'success', '読み込みが完了しました。', {
                        type: 'snackbar',
                    } )
                }
            >
                通知を出す
            </Button>
        </div>
    );
}

ストアを指定するとき、useDispatch( 'core/notices' ) のように文字列を直接書くこともできます。ただし文字列はタイプミスをしても気づけないので、import { store as noticesStore } from '@wordpress/notices' でストア定義を読み込み、それを渡す書き方が推奨されています。この記事でもその形で統一します。

useDispatch は React のフックなので、コンポーネント関数の一番外側(トップレベル)で呼ぶ必要があります。onClick の中やイベントハンドラーの内側で呼ぶことはできません。上の例のように、先に createNotice を取り出しておいて、ハンドラーからはその変数を呼ぶ形にしてください。

createNotice の引数とオプション

createNotice は3つの引数を取ります。第1引数が通知の種類(ステータス)、第2引数が表示する文言、第3引数が細かい指定をまとめたオブジェクトです。

createNotice のシグネチャ
createNotice( status, content, options );

// status  : 'success' | 'info' | 'warning' | 'error'(省略時は 'info')
// content : 表示する文言(文字列)
// options : 表示方法を指定するオブジェクト(省略可)

ステータスは4つあり、バナーとして表示したときの色とアイコンがそれぞれ変わります。

status想定する場面
'success'処理が成功したとき(保存できた、取得できた)
'info'結果の良し悪しを伴わないお知らせ。省略時の既定値
'warning'致命的ではないが注意してほしいとき(設定が未入力など)
'error'処理が失敗したとき

第3引数のオプションで、表示場所や消し方を細かく指定できます。よく使うものは次のとおりです。

オプション既定値説明
type'default''default' はエディター上部のバナー、'snackbar' は画面下部に数秒だけ出て自動的に消える小さな通知
id自動採番通知の識別子。同じ id で作り直すと置き換わり、removeNotice で消すときにも使う
isDismissibletrueユーザーが閉じられるようにするか。false にすると閉じるボタンが出ない
actions[]通知に添えるボタンやリンクの配列
explicitDismissfalseスナックバーのとき、本体をクリックしても閉じず、専用の閉じるボタンでのみ閉じられるようにする
iconnull通知に添えるアイコン。type が 'snackbar' のときだけ使われる
onDismiss—通知が閉じられたときに呼ばれる関数
speaktrueスクリーンリーダーに内容を読み上げさせるか
context'global'通知をグループ分けするための文脈。エディターに表示したいなら既定のままにする

バナーとスナックバーの使い分け

type を省略するとエディター上部にバナーが出ます。バナーは自分で閉じるまで残るので、ユーザーに対処してほしいエラーや警告に向いています。一方 type: 'snackbar' は時間が経つと自動的に消えるため、「保存できた」「コピーした」のように読み飛ばしても困らない報告に向いています。

スナックバーは表示面積が小さく、ステータスによる色分けもバナーほどはっきりしません。エラーを伝えたいときは type を省いてバナーで出すほうが、見落とされにくくなります。

actions で操作ボタンやリンクを添える

actions には、通知の中に並べるボタンやリンクをオブジェクトの配列で渡します。onClick を持たせればボタン、url を持たせればリンクとして描画されます。

src/my-block/edit.js(actions の指定)
createNotice( 'warning', 'この API キーはまだ設定されていません。', {
    actions: [
        {
            // onClick を持たせるとボタンになる
            label: '既定値を入れる',
            onClick: () => setAttributes( { apiKey: 'demo-key' } ),
        },
        {
            // url を持たせるとリンクになる
            label: '設定画面を開く',
            url: '/wp-admin/options-general.php',
        },
    ],
} );

label は必須です。ボタンを押しても通知は自動では消えないので、押したあとに引っ込めたい場合は、後述する removeNotice を onClick の中から呼びます。

ステータスごとのショートハンド

毎回 createNotice( 'success', ... ) と書くのは冗長なので、ステータスをあらかじめ決めた4つの関数が用意されています。こちらは第1引数が文言、第2引数がオプションという並びになる点に注意してください。

関数相当する呼び出し
createSuccessNotice( content, options )createNotice( 'success', content, options )
createInfoNotice( content, options )createNotice( 'info', content, options )
createWarningNotice( content, options )createNotice( 'warning', content, options )
createErrorNotice( content, options )createNotice( 'error', content, options )
src/my-block/edit.js(ショートハンド)
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );

// 成功はスナックバーで軽く伝える
createSuccessNotice( '設定を保存しました。', { type: 'snackbar' } );

// 失敗は上部のバナーで残す
createErrorNotice( '設定を保存できませんでした。' );

オプションの指定方法は createNotice とまったく同じです。引数の並びが1つずれるだけなので、createSuccessNotice( { type: 'snackbar' } ) のようにオプションを第1引数に渡してしまう書き間違いには注意してください。文言が空のまま通知が出てしまいます。

id を付けて重複を防ぎ、removeNotice で消す

id を省略すると、呼ぶたびに新しい通知が積み上がります。ボタンを3回押せばバナーも3本並ぶ、ということです。同じ種類の通知を1本だけに保ちたいときは、id を指定します。同じ id の通知が既にある場合は新しいものに置き換わるので、連打されても増えません。

通知を消すのは removeNotice( id ) です。こちらも useDispatch から取り出します。id は自分で付けたものと一致させる必要があるため、文字列は定数にまとめておくと安全です。

src/my-block/edit.js(id と removeNotice)
const NOTICE_ID = 'my-blocks/fetch-status';

const { createErrorNotice, removeNotice } = useDispatch( noticesStore );

// 何度押しても1本だけ表示される
const showError = () => {
    createErrorNotice( 'データを取得できませんでした。', {
        id: NOTICE_ID,
    } );
};

// 再取得に成功したら自分で引っ込める
const clearError = () => {
    removeNotice( NOTICE_ID );
};

id はグローバルに共有される空間なので、他のプラグインとぶつからないように my-blocks/fetch-status のようなプレフィックス付きの名前にしておくと安心です。存在しない id を removeNotice に渡しても、エラーにはならず何も起きません。

実践例:API の呼び出し結果を通知する

ここまでの内容を組み合わせて、ブロックのボタンから REST API を叩き、成功したらスナックバー、失敗したらエラーバナーを出す edit.js を書いてみます。通信には @wordpress/api-fetch を使い、失敗時には actions で再試行ボタンを添えます。

src/latest-post/edit.js
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { Button, Placeholder } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import apiFetch from '@wordpress/api-fetch';
import { __, sprintf } from '@wordpress/i18n';

const NOTICE_ID = 'my-blocks/latest-post';

export default function Edit( { attributes, setAttributes } ) {
    const { title } = attributes;
    const blockProps = useBlockProps();
    const [ isLoading, setIsLoading ] = useState( false );

    const { createSuccessNotice, createErrorNotice, removeNotice } =
        useDispatch( noticesStore );

    const fetchLatestPost = async () => {
        setIsLoading( true );

        // 前回のエラーが残っていれば先に消す
        removeNotice( NOTICE_ID );

        try {
            const posts = await apiFetch( {
                path: '/wp/v2/posts?per_page=1&_fields=title',
            } );

            if ( ! posts.length ) {
                throw new Error( __( '記事が見つかりません。', 'my-blocks' ) );
            }

            setAttributes( { title: posts[ 0 ].title.rendered } );

            // 成功はスナックバーで軽く知らせる
            createSuccessNotice(
                __( '最新記事のタイトルを読み込みました。', 'my-blocks' ),
                {
                    type: 'snackbar',
                    id: NOTICE_ID,
                }
            );
        } catch ( error ) {
            // 失敗は上部のバナーに残し、再試行ボタンを添える
            createErrorNotice(
                sprintf(
                    /* translators: %s: エラーメッセージ */
                    __( '読み込みに失敗しました: %s', 'my-blocks' ),
                    error.message
                ),
                {
                    id: NOTICE_ID,
                    actions: [
                        {
                            label: __( '再試行', 'my-blocks' ),
                            onClick: fetchLatestPost,
                        },
                    ],
                }
            );
        } finally {
            setIsLoading( false );
        }
    };

    return (
        <div { ...blockProps }>
            { title ? (
                <p>{ title }</p>
            ) : (
                <Placeholder label={ __( '最新記事', 'my-blocks' ) }>
                    <Button
                        variant="primary"
                        isBusy={ isLoading }
                        disabled={ isLoading }
                        onClick={ fetchLatestPost }
                    >
                        { __( '読み込む', 'my-blocks' ) }
                    </Button>
                </Placeholder>
            ) }
        </div>
    );
}

成功と失敗で同じ id を使っているのがポイントです。エラーが出たあとに再試行して成功すると、エラーバナーは成功のスナックバーに置き換わるため、古い通知が残り続けることがありません。catch の中で error.message を使っているのは、apiFetch が REST API のエラーレスポンスを message プロパティを持つオブジェクトとして投げるためです。

保存が終わったタイミングで通知を出す

ブロック単位ではなく、エディター全体に対して通知を出したいこともあります。その場合は @wordpress/plugins の registerPlugin で、画面には何も描画しないコンポーネントを登録するのが定番です。次の例は、投稿の保存が完了したタイミングでスナックバーを出すものです。

src/save-watcher.js
import { registerPlugin } from '@wordpress/plugins';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { store as noticesStore } from '@wordpress/notices';
import { useEffect, useRef } from '@wordpress/element';

function SaveWatcher() {
    const { createSuccessNotice } = useDispatch( noticesStore );

    // 保存中かどうかを監視する
    const isSaving = useSelect(
        ( select ) => select( editorStore ).isSavingPost(),
        []
    );

    // 直前の状態を覚えておく
    const wasSaving = useRef( false );

    useEffect( () => {
        // 「保存中だった」→「保存中でなくなった」= 保存が終わった瞬間
        if ( wasSaving.current && ! isSaving ) {
            createSuccessNotice( '保存処理が完了しました。', {
                type: 'snackbar',
                id: 'my-plugin/after-save',
            } );
        }

        wasSaving.current = isSaving;
    }, [ isSaving, createSuccessNotice ] );

    // 通知を出すだけなので何も描画しない
    return null;
}

registerPlugin( 'my-plugin-save-watcher', { render: SaveWatcher } );

registerPlugin の第1引数は、小文字とハイフンだけで書く一意な名前です。第2引数の render に渡したコンポーネントはエディターの中で常に描画されるので、null を返しておけば「通知を出すためだけの常駐コンポーネント」として使えます。このファイルは block.json の editorScript か、enqueue_block_editor_assets で読み込んだスクリプトから実行してください。

isSavingPost() の値だけを見ると true の間ずっと条件に合ってしまうため、useRef で直前の値を保持して「true から false に変わった瞬間」を検出しています。保存が成功したかどうかまで区別したい場合は、同じ editor ストアの didPostSaveRequestSucceed() や didPostSaveRequestFail() を組み合わせます。

通知が表示されないとき

createNotice はストアに値を追加するだけの関数なので、呼び出しそのものが失敗することはほとんどありません。「呼んでいるはずなのに出ない」ときは、呼び出しの手前か、表示する側のどちらかで条件がずれています。よくある原因を順番に見ていきましょう。

useDispatch をコンポーネントの外で呼んでいる

useDispatch は React のフックなので、コンポーネント関数の内側のトップレベル、またはカスタムフックの中でしか呼べません。イベントハンドラーの中や、モジュールの最上位(import のすぐ下)で呼ぶとフックのルール違反になり、コンソールに警告が出たうえで正しく動きません。

どうしてもコンポーネントの外から通知を出したい場合は、フックではない dispatch を使います。こちらは @wordpress/data から直接 import でき、どこからでも呼べます。

src/utils.js(コンポーネント外から通知を出す)
import { dispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

// フックではないので、ただの関数の中からでも呼べる
export function notifyError( message ) {
    dispatch( noticesStore ).createErrorNotice( message, {
        id: 'my-blocks/util-error',
    } );
}

スクリプトの依存に wp-notices が入っていない

@wordpress/notices は、ビルド後には window.wp.notices を参照するコードに変換されます。このオブジェクトは wp-notices というスクリプトが読み込まれて初めて存在するため、依存関係に入っていないと Cannot read properties of undefined のようなエラーになり、そこから先のコードが動きません。

@wordpress/scripts(wp-scripts build)でビルドしていれば、import した WordPress パッケージは自動的に依存として抽出され、index.asset.php に書き出されます。block.json の editorScript に file:./index.js を指定していれば、WordPress がこのファイルを読んで依存を設定してくれるので、自分で書く必要はありません。

問題が起きるのは、ビルドを通さず素の JS を wp_enqueue_script() で読み込んでいる場合です。このときは第3引数の依存配列に 'wp-notices' と 'wp-data' を自分で書き足してください。

context を既定値から変えている

通知は context というキーでグループ分けされて保存されます。エディターが画面に描画しているのは 'global' のグループだけなので、options.context に独自の文字列を指定すると、ストアには保存されても画面には出てきません。

context は、自分で getNotices( context ) を呼んで独自の場所に表示するとき(たとえば自作の設定画面で SnackbarList を描画するとき)のための仕組みです。エディターに出したいだけなら、必ず既定の 'global' のままにしてください。

フロント側や save.js から呼ぼうとしている

core/notices はエディターの画面を構成するストアです。通知を表示しているのはエディター側のコンポーネントなので、公開ページ(フロントエンド)には通知は出ません。フロントでメッセージを出したい場合は、自分で要素を用意する必要があります。

同じ理由で、save.js の中で createNotice を呼ぶのも誤りです。save は投稿内容として保存する HTML を組み立てるための関数で、エディターの状態を書き換える処理を入れてはいけません。通知は edit.js 側、つまり編集中の画面を担当するコードから呼びます。

スナックバーが一瞬で消えて読めない

スナックバーは一定時間で自動的に消えます。長い文章を入れても読み切る前に消えてしまうので、スナックバーに渡す文言は一文で完結する短いものにしてください。詳しい説明が必要な内容は type を省略してバナーで出すほうが適切です。

どうしてもスナックバーの形で残したい場合は explicitDismiss: true を指定します。閉じるボタンが表示され、ユーザーがそれを押すまで表示され続けます。

まとめ

ブロックエディターで通知を出したいときは、@wordpress/notices のストアを useDispatch で取り出し、createNotice( status, content, options ) を呼ぶだけです。ステータスが決まっている場面では createSuccessNotice や createErrorNotice といったショートハンドを使うと読みやすくなります。こちらは第1引数が文言である点だけ気をつけてください。

表示場所は type で切り替えます。自動で消える 'snackbar' は成功の報告に、残り続ける既定のバナーはエラーや警告に向いています。id を付けておけば同じ通知が積み上がるのを防げ、removeNotice( id ) で任意のタイミングで消せます。alert() をやめてこの仕組みに寄せるだけで、自作ブロックの使い心地はぐっと WordPress らしくなります。

参考ページ