1. ホーム
  2. WordPress

【WordPress】apiFetch の使い方|ブロックエディターから REST API を呼び出す方法

Share

カスタムブロックの編集画面に「サーバーから取ってきた値」を出したくなることがあります。投稿の件数、外部サービスの状態、独自テーブルに保存した設定などです。こうしたときにブラウザーの fetch() をそのまま書くと、認証で弾かれたりパスの組み立てを毎回書くことになったりします。WordPress のブロックエディターには、この用途のための apiFetch(@wordpress/api-fetch)が用意されています。この記事では基本の呼び出し方から、独自エンドポイントの作り方、うまくいかないときの確認どころまでを順に解説します。

apiFetch が fetch の代わりに引き受けてくれること

apiFetch は内部で fetch() を呼ぶラッパーです。ただの薄い包み紙ではなく、WordPress の REST API を叩くうえで毎回必要になる準備を肩代わりしてくれます。大きいのは次の3点です。

ひとつ目は nonce の付与です。ログイン中のユーザーとして REST API を叩くには、Cookie 認証に加えて X-WP-Nonce ヘッダーが必要です。apiFetch はこのヘッダーを自動で付けるため、下書きの取得や投稿の更新のような「権限が要る操作」がそのまま通ります。fetch() を直接書くと、ここを自前で用意しない限り 401 が返ってきます。

ふたつ目は REST ルートの解決です。path に /wp/v2/posts と書けば、そのサイトの REST API の入口(https://example.com/wp-json/ やパーマリンク設定によっては ?rest_route= 形式)を前に付けた URL に組み立ててくれます。サイトごとに違う部分を JavaScript 側に書かずに済みます。

みっつ目は レスポンスの扱いです。fetch() は HTTP のステータスが 404 や 500 でも「通信としては成功」とみなすため、response.ok を自分で見る必要があります。apiFetch は JSON を解析したうえで、エラー応答なら Promise を reject します。つまり then() にはデータだけが、catch() にはエラーだけが流れてきます。

useSelect との使い分け

投稿・固定ページ・タクソノミー・ユーザーといったWordPress 標準のデータを読むだけなら、useSelect と core ストアの getEntityRecords() を使うほうが向いています。取得結果がストアにキャッシュされ、他のブロックが同じデータを求めても通信が1回で済むためです。

apiFetch の出番は、ストアが面倒を見ていないエンドポイントを叩くときです。自分のプラグインで register_rest_route() して作ったエンドポイント、集計値だけを返す軽い API、ボタンを押した瞬間に一度だけ実行したい処理などが該当します。以降の例もこの前提で書きます。

基本の使い方

パッケージから読み込んで、オプションのオブジェクトを渡して呼びます。戻り値は Promise です。

src/edit.js
import apiFetch from '@wordpress/api-fetch';

// 最新5件の投稿を取得する
apiFetch( { path: '/wp/v2/posts?per_page=5' } )
    .then( ( posts ) => {
        console.log( posts ); // 投稿オブジェクトの配列
    } )
    .catch( ( error ) => {
        console.error( error.message );
    } );

path は先頭のスラッシュから書きます。クエリ文字列はそのまま ?per_page=5 のように付けて構いません。メソッドを省略した場合は GET です。

ビルドに @wordpress/scripts(wp-scripts build)を使っていれば、この import を書くだけで wp-api-fetch が依存スクリプトとして *.asset.php に書き出されます。wp_enqueue_script() にその依存配列を渡している限り、読み込みの追加設定は要りません。

主なオプション

渡せるオプションのうち、よく使うものをまとめます。ここに挙げていないキー(credentials や mode など)は、そのまま fetch() に渡されます。

オプション説明
pathREST API のパス。/wp/v2/posts のようにルート名前空間から書く
url完全な URL を直接指定する。path と併用はしない
methodHTTP メソッド。'POST' / 'PUT' / 'DELETE' など。省略時は 'GET'
data送信するデータ。JSON 文字列に変換され、Content-Type: application/json が付く
headers追加のリクエストヘッダーをオブジェクトで指定する
parsefalse にすると解析前の Response オブジェクトが返る。既定は true
signalAbortController のシグナル。途中でリクエストを中断できる

データを送る(POST)

更新系は method と data を指定します。data は自分で JSON.stringify() せず、オブジェクトのまま渡します。

src/edit.js
// 投稿 ID 123 のタイトルを更新する
apiFetch( {
    path: '/wp/v2/posts/123',
    method: 'POST',
    data: {
        title: '新しいタイトル',
        status: 'draft',
    },
} ).then( ( updated ) => {
    console.log( updated.title.rendered );
} );

WordPress の REST API は更新にも POST を受け付けます(PUT や PATCH も使えます)。削除は method: 'DELETE' です。

ブロックの編集画面で使う

実際のブロックでは useState と useEffect を組み合わせ、「読み込み中」「取得済み」「失敗」の3つの状態を出し分けるのが基本形です。ここでは、あとで作る独自エンドポイントから投稿数を取ってきて表示するブロックを例にします。

src/edit.js
import apiFetch from '@wordpress/api-fetch';
import { useBlockProps } from '@wordpress/block-editor';
import { useState, useEffect } from '@wordpress/element';
import { Spinner } from '@wordpress/components';

export default function Edit() {
    const blockProps = useBlockProps();
    const [ stats, setStats ] = useState( null );
    const [ error, setError ] = useState( null );

    useEffect( () => {
        // 後片付け用のフラグ。取得中にブロックが消えた場合に備える
        let ignore = false;

        apiFetch( { path: '/my-plugin/v1/stats' } )
            .then( ( data ) => {
                if ( ! ignore ) {
                    setStats( data );
                }
            } )
            .catch( ( err ) => {
                if ( ! ignore ) {
                    setError( err.message );
                }
            } );

        return () => {
            ignore = true;
        };
    }, [] ); // 第2引数が空配列なので最初の1回だけ実行される

    if ( error ) {
        return <p { ...blockProps }>{ error }</p>;
    }

    if ( ! stats ) {
        return (
            <p { ...blockProps }>
                <Spinner />
            </p>
        );
    }

    return (
        <p { ...blockProps }>
            公開済み { stats.published } 件 / 下書き { stats.draft } 件
        </p>
    );
}

ignore というフラグを使っているのは、リクエストの返事が返る前にブロックが削除されると、存在しない要素に対して setStats() を呼ぶことになるためです。React の後片付け関数でフラグを立て、返事が返ってきたときに無視するようにしています。中断まで確実に行いたい場合は AbortController を作り、signal オプションに渡して後片付けで abort() を呼びます。

なお、useEffect の第2引数に指定した値(依存配列)が変わるたびに再取得されます。ブロックの属性に応じて内容を取り直したいときは、その属性を配列に入れておきます。

独自エンドポイントを作って呼ぶ

呼び出し先は PHP 側で register_rest_route() を使って登録します。第1引数が名前空間(ベンダー名/バージョン の形が慣習)、第2引数がルート、第3引数が設定です。

my-plugin.php
add_action( 'rest_api_init', 'my_plugin_register_routes' );

function my_plugin_register_routes() {
    register_rest_route(
        'my-plugin/v1',           // 名前空間
        '/stats',                 // ルート
        array(
            'methods'             => 'GET',
            'callback'            => 'my_plugin_get_stats',
            // 誰が呼べるかを必ず指定する
            'permission_callback' => function () {
                return current_user_can( 'edit_posts' );
            },
        )
    );
}

function my_plugin_get_stats( WP_REST_Request $request ) {
    $counts = wp_count_posts( 'post' );

    return rest_ensure_response(
        array(
            'published' => (int) $counts->publish,
            'draft'     => (int) $counts->draft,
        )
    );
}

これで JavaScript 側の path: '/my-plugin/v1/stats' が、https://example.com/wp-json/my-plugin/v1/stats に解決されます。permission_callback は省略できません。省略すると WordPress が注意(_doing_it_wrong)を出し、誰でも叩ける状態になってしまいます。ログイン中のユーザーであれば良い、という緩い条件でも 'permission_callback' => 'is_user_logged_in' のように明示します。

パラメーターを受け取る

ルートに正規表現を書けば、パスの一部を引数として受け取れます。クエリ文字列や data で送った値も同じく WP_REST_Request から読めます。

my-plugin.php
register_rest_route(
    'my-plugin/v1',
    '/stats/(?P<type>[a-z_-]+)',
    array(
        'methods'             => 'GET',
        'callback'            => function ( WP_REST_Request $request ) {
            $type   = $request['type'];        // パスの一部
            $limit  = $request->get_param( 'limit' ); // クエリ文字列
            $counts = wp_count_posts( $type );

            return rest_ensure_response(
                array(
                    'type'      => $type,
                    'limit'     => (int) $limit,
                    'published' => (int) $counts->publish,
                )
            );
        },
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
        'args'                => array(
            'limit' => array(
                'default'           => 10,
                'sanitize_callback' => 'absint',
            ),
        ),
    )
);

JavaScript 側からは apiFetch( { path: '/my-plugin/v1/stats/page?limit=5' } ) と呼びます。args に sanitize_callback や validate_callback を書いておくと、コールバックに渡る前に値を整えたり弾いたりできます。

ヘッダーを読みたいときは parse を切る

ページ送りのある一覧を扱うと、総件数や総ページ数が本文ではなく X-WP-Total / X-WP-TotalPages というレスポンスヘッダーに入ってきます。既定の apiFetch は本文の JSON だけを返すため、このままではヘッダーを見られません。parse: false を指定すると、解析前の Response オブジェクトが返ります。

src/edit.js
apiFetch( {
    path: '/wp/v2/posts?per_page=10',
    parse: false,
} ).then( async ( response ) => {
    const totalPages = response.headers.get( 'X-WP-TotalPages' );
    const posts = await response.json(); // 本文は自分で解析する

    console.log( `全 ${ totalPages } ページ、このページは ${ posts.length } 件` );
} );

parse: false にすると、エラー応答を自動で reject する働きも無効になります。ステータスの確認(response.ok)は自分で書く必要がある点に注意してください。

apiFetch.use で全リクエストに処理を挟む

apiFetch はミドルウェア方式になっていて、apiFetch.use() で登録した関数がすべての呼び出しを通過します。ログを取る、共通のヘッダーを足す、といった処理をまとめて書けます。関数は options と next を受け取り、next( options ) の戻り値(Promise)を返します。

src/index.js
import apiFetch from '@wordpress/api-fetch';

// 自分のプラグインのリクエストだけ所要時間をログに出す
apiFetch.use( ( options, next ) => {
    if ( ! options.path?.startsWith( '/my-plugin/' ) ) {
        return next( options ); // 対象外はそのまま通す
    }

    const start = Date.now();

    return next( options ).then( ( result ) => {
        console.log( `${ options.path }: ${ Date.now() - start }ms` );
        return result;
    } );
} );

自分に関係ないリクエストは必ず next( options ) でそのまま流します。ここで止めてしまうと、エディター本体が使っている保存や自動保存まで巻き込んで壊れます。用途を絞りたいときは、上の例のように options.path で判定するのが安全です。

WordPress 自身も同じ仕組みでいくつかのミドルウェアを最初から登録しています。createNonceMiddleware()(nonce の付与)や createRootURLMiddleware()(REST の入口の解決)がそれで、冒頭で挙げた「肩代わり」の正体はこれらです。エディター以外の画面から apiFetch を使いたい場合は、これらを自分で登録することになります。

リクエストが失敗するときの確認どころ

catch() に渡ってくるエラーは、REST API が返した JSON がそのまま入ったオブジェクトです。error.code にエラーの識別子、error.message に説明、error.data.status に HTTP ステータスが入っています。原因の切り分けはこの code を見るところから始めます。

rest_no_route が返る

指定したパスにルートが見つかっていない状態です。多いのは、register_rest_route() を rest_api_init フックの中で呼んでいないケースです。プラグインの読み込み時にいきなり呼んでも、REST API の初期化前なので登録されません。また、名前空間とルートは連結されるため、名前空間 my-plugin/v1 とルート /stats に対する path は /my-plugin/v1/stats です。/wp-json/ を path に含めると二重になって同じエラーになります。

登録できているかどうかは、ブラウザーで https://example.com/wp-json/my-plugin/v1 を開くと確認できます。ルートの一覧が JSON で返れば登録は成功しています。

rest_forbidden や 403 が返る

permission_callback が false を返しています。current_user_can() に渡している権限を見直してください。よくあるのは、寄稿者や購読者のロールでも使わせたい機能に manage_options(管理者のみ)を指定してしまっているパターンです。特定の投稿に紐づく操作なら current_user_can( 'edit_post', $post_id ) のように対象を渡す形にします。

rest_cookie_invalid_nonce が返る

nonce が無効になっています。nonce には有効期限(既定でおよそ12〜24時間)があるため、編集画面を開いたまま長時間放置したあとの操作で起きがちです。画面を再読み込みすれば直ります。

再読み込みしても直らない場合は、そもそも nonce が付いていない可能性があります。スクリプトの依存配列に wp-api-fetch が入っているか確認してください。@wordpress/scripts のビルドを使わずに手書きで wp_enqueue_script() している場合、この依存を書き忘れると nonce を設定するインライン スクリプトが出力されません。ログイン画面に飛ばされる、管理画面以外のページから叩いている、という場合も同様に nonce が無い状態です。

HTML が返ってきて JSON の解析に失敗する

コールバックの中で PHP のエラーや echo が起きていると、JSON の前にその出力が混ざって解析に失敗します。WP_DEBUG を有効にしてエンドポイントの URL を直接ブラウザーで開き、返ってきている中身を目で確かめるのが早道です。var_dump() の消し忘れが原因、ということも珍しくありません。

まとめ

apiFetch は、nonce の付与と REST ルートの解決、エラー応答の reject を引き受けてくれる REST API 用の fetch() です。path にパスを、更新系なら method と data を渡すだけで呼び出せます。標準のデータを読むだけなら useSelect と core ストアのほうが向いていますが、register_rest_route() で作った独自エンドポイントを叩くなら apiFetch が素直です。ブロックの中では useState と useEffect を組み合わせ、読み込み中と失敗の表示も用意しておくと扱いやすくなります。うまく動かないときは error.code を見て、rest_no_route なら登録の場所とパス、rest_forbidden なら permission_callback、rest_cookie_invalid_nonce なら依存スクリプトの指定、と順に当たってみてください。

参考ページ