Blog

WordPressでページ遷移せずにデータを取得する。(WP + React + esbuild)

Webサイトを作っていると、何かボタンをクリックした時に、ページ遷移することなくデータがその場で入れ替わる…といった動きが欲しい時があります。

例えば、条件を選んでいって「検索」ボタンを押すと、登録している商品の中から条件に合ったものを、ページ遷移せずに表示してくれるといったものです。

概要

WordPressは、カテゴリーテンプレートやその他たくさんのテンプレートが用意されていて、
カテゴリーごとに投稿が表示されたページなどはデフォルトで表示可能ですが、
あくまでページを移動せずにその場でスマートにWordPressのデータを表示するにはどうしたら良いか。

この時に使えるのが「WP REST API」です。

元々WordPressは、任意の条件で投稿を取得する機能が豊富に揃っていますが、
それを使ってエンドポイントを作ることができ、エンドポイントがあればフロント側でなんとでも動きを実装することができるというわけです。

なんとでも…と書いていますが、特にカンタンというわけでもありません。
今回はReactとesbuildを使ってフロントを実装します。

基本的にReactを使ったことがない方でもわかる、コーダー向けの内容にしたいと思っています。
この記事は、下記の方が対象です。

  • WordPressで、WP_Queryを使ったコードが書ける
  • なんらかのビルドツールを使っているか、知見がある(この記事ではGulpを前提にしています)
  • JavaScriptがわかる

jsのビルドはgulp-esbuildを使っています。
Gulpでesbuildを使うための記事はこちらです。

https://cumak.net/blog/gulp-esbuild/

デモ

WordPressには、下記の記事を登録しています。

  • カテゴリー → 果物、野菜
  • タグ → 黄色、赤、青
  • 投稿
    • バナナ(果物で黄色)
    • りんご(果物で赤)
    • 黄ピーマン(野菜で黄色)
    • 赤ピーマン(野菜で赤)

ラジオボタンで各条件を選択して、投稿を表示させています。

HTML

今回は固定ページのテンプレートを使うことにします。
page-diagnosis.php に、フォームの部分と、結果が表示される部分を作ります。

フォーム部分は、inputタグなりフォームのタグを使い、CSSは自由にスタイリングすればOKです。カテゴリをループするなども自由にしてください。

後にフォームの値の内容をJSで取得できる形になっていれば良いです。
ここではわかりやすさ重視で、ベタ打ちでvalueにIDを入れるということをしています。

<section>
  <h2>種類(カテゴリー)を選んでください。</h2>
  <form id="cateForm">
    <!-- ここは、カテゴリをループするなりなんなりと。ここでは固定でidを送信する形にしています -->
    <div>
      <input id="fruit" type="radio" name="cate" value="1">
      <label for="fruit">果物</label>
    </div>
    <div>
      <input id="vesitable" type="radio" name="cate" value="3">
      <label for="vesitable">野菜</label>
    </div>
  </form>
</section>
<section>
  <h2>色(タグ)を選んでください</h2>
  <form id="tagForm">
    <!-- ここは、タグをループするなりなんなりと。ここでは固定でidを送信する形にしています -->
    <div>
      <input id="yellow" type="radio" name="tag" value="4">
      <label for="yellow">黄色</label>
    </div>
    <div>
      <input id="red" type="radio" name="tag" value="5">
      <label for="red">赤</label>
    </div>
    <div>
      <input id="blue" type="radio" name="tag" value="7">
      <label for="blue">青</label>
    </div>
  </form>
</section>

<button id="searchBtn">検索する</button>

フロントの実装は、そのページ専用のJSファイルを作って、scriptタグで読み込むことにします。
diagnosis.jsというファイルをあとで作るので、読み込んでおきます。

<script src="<?php echo get_theme_file_uri('/js/diagnosis.js') ?>" defer></script>

ローカルや仮環境に対応するため、ドメインパスを取得してhiddenで忍ばせておきます。

<input type="hidden" id="domainPath" value="<?php echo esc_url(home_url('/')) ?>">

検索結果を表示するところは、divを作ってIDを付与しておきます。
中身はReactのコンポーネントで作ります。

<div id="diagnosisArea"></div>

esbuildの設定

reactは、scriptタグで読み込んで使うこともできますが、esbuildやwebpackなどのビルドツールを利用すると必要な機能だけimportして実装することができます。軽量化につながるので、reactの公式でもそちらを推奨されています。
また、reactについてのドキュメントや、Webに転がっている知識は、ほとんどがビルドを前提にしたものですので、ビルド環境を作ることにします。

gulpを使っているならgulpでwebpackやesbuildを動かすこともできます。
ここではgulp+esbuildで進めていますが、gulpを経由しなくても同じです。

一番ハマったところなのですが、esbuildのオプションに

define: { "process.env.NODE_ENV": process.env.NODE_ENV },

を設定しておく必要があります。

gulp-esbuildを使っている場合は、下記のような形になります。
(必要なところだけ抜粋しています)

const gulpEsbuild = require('gulp-esbuild')

const esbuildDiagnosis = () => {
  return gulp.src(baseDir + '/js/diagnosis.jsx')
    .pipe(gulpEsbuild({
      bundle: true,
      define: { "process.env.NODE_ENV": process.env.NODE_ENV },
      outfile: "diagnosis.js",
      minify: true,
      target: [
        'es2020',
      ],
    }))
    .pipe(gulp.dest(dist + '/出力したいフォルダ/js/'))
    .pipe(browserSync.stream());
}

const watch = () => {
  gulp.watch(baseDir + '/js/diagnosis.jsx', gulp.series('esbuild-diagnosis'));
};

gulp.task('esbuild-diagnosis', esbuildDiagnosis);

exports.default = gulp.parallel('watch');

ここで、ビルドの元となるファイルはjsxという拡張子にしています。TypeScriptの場合はtsxです。
これは、ReactのコンポーネントでJSXを使うためです。JSX はJavaScript の構文の拡張で、HTMLのような構文が使えるようにしたもので、UI 要素を記述するのに使います。

仮に拡張子をjsにして進めると、コンパイル時に

ERROR: The JSX syntax extension is not currently enabled

とエラーが出ます。

WordPressのRest API

どうやってWordPressのデータベースから情報を取得するのかというと、
データ取得専用のURIを作成します。
APIにアクセスするための機能がついてるURIをエンドポイントといいます。

WordPressのデフォルトのエンドポイントを確認する

WordPressは、REST APIのエンドポイントを用意しています。
ブラウザで下記URLをたたくことによって、返ってくるJSONを確認できます。

https://WordPressルート/wp-json/wp/v2/

画面にJSONがでてきますが、Chromeであれば、開発者ツールの
「ネットワーク」→「v2」選択→「プレビュー」タブ
をみてみると、いろんなエンドポイントが用意されていることがわかります。

例えば、

https://WordPressルート/wp-json/wp/v2/pages/

を叩いてみると、固定ページの記事情報が表示されます。
これらを使ってデータを取得することもできます。

自分のほしいエンドポイントを作る拡張機能

WordPressのRest APIは、拡張してエンドポイントを追加することができます。
ここで強力なのが、テンプレートのサブループの要領で、任意の条件を作成してループを回し、データを選別することができるということです。

つまり、WP_Queryで作ることができる条件ならどんなパターンでも返すことができるので、
カテゴリもタグも、カスタム投稿もカスタムフィールドで作りたい結構複雑な条件でも、
WordPressの知識さえあれば返すことが可能です。

WordPress公式 Adding Custom Endpoints

register_rest_routeという関数を使って、エンドポイントを作ります。
rest_api_initのコールバックで呼び出すと、REST APIが初期化されたタイミングで呼び出され、APIがロードされていないときに余計な作業をしないようにできます。

シンプルなregister_rest_route関数の使い方は、以下です。
第一引数は名前空間、第二引数は必要なルート、第三引数はオプションです。

<?php
add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_func',
  ) );
} );

上記の例では、/author/(?P<id>\d+)というルートに対して、エンドポイント(GETメソッドとmy_awesome_funcというコールバック関数)を登録したことになります。
(?P<id>\d+)の部分はパラメータです。URIの末尾にidを入れることで、任意のidを渡すことができます。

結果、下記のようなURIで、idが1のauthorの情報を取得することができます。

http://example.com/wp-json/myplugin/v1/author/1

第一引数の名前空間は自由に決めることができますが、一般的に名前空間は vendor/v1 のパターンに従うべきだとされているようです。
vendorは通常プラグインやテーマのスラッグで、v1はバージョン1という意味ですが、
今回の用途においては、だれか他にこのAPIを参照したい人がいるわけではないので、わかりやすい名前で良いと思います。

functions.phpにて、エンドポイントを作成

今回はカスタム投稿は使用しませんが、カスタム投稿を使用する場合は
show_in_resttrueにしておく必要があります。

'show_in_rest' => true,

今回は、カスタム投稿を使用せず、カテゴリとタグでループさせるものを作ります。

if(!is_admin()){
  add_action('rest_api_init',function(){
    register_rest_route(
      // 第一引数 : 名前空間
      'wp/v1',
      // 第二引数 : エンドポイント
      '/items',
      // 第三引数 : メソッドなどの設定配列
      array(
        'method' => 'GET',
        'args' => array(
          'paged' => array(
            'default' => 1,
            'sanitize_callback' => 'absint'
          )
        ),
        'callback' => function($r){
          // パラメータ取得
          $categoryId = $r -> get_param('cate');
          $tagId = $r -> get_param('tag');

          // WP_Queryで、好きなように条件を作る
          $query = new WP_Query(array(
            'cat' => $categoryId,
            'tag_id' => $tagId,
            'posts_per_page' => -1,
            'paged' => $r -> get_param('paged'),
          ));
          // 条件に合った投稿の、必要なもの(IDやタイトルなど)を$itemsという配列に詰める。
          $items = array();
          while($query->have_posts()){
            $query -> the_post();
            $item = array();
            $item['id'] = get_the_ID();
            $item['title'] = get_the_title();
            $item['permalink'] = get_the_permalink();
            $items[] = $item;
          }
          return $items;
        }
      )
    );
  });
}

これで、カテゴリIDとtagIDで記事を取得するエンドポイントが完成しました。

成功していれば、ブラウザのURL部分に下記のようにエンドポイントパスを入れると、当てはまる投稿のJSONが返ってきます。

/wp-json/wp/v1/items/1?cate=1&tag=4

上記では、カテゴリIDが1且つタグIDが4のものを取得することができます。

パッケージをインストールする

npmやyarnでreactとreact-domをインストールします。
また、DBと通信するためにaxiosを使用します。

npm i react
npm i react-dom
npm i axios

検索結果を取得する

JSでフロントエンドの処理を記載する全コード

diagnosis.jsxというファイルを作って、ここにフロントエンドの処理を書いていきます。
上の方で用意したesbuildの元ファイルです。

とりあえずコードを載せます。

import axios from 'axios'
import React from 'react';
import ReactDOM from "react-dom";
import { useState,useEffect } from "react";
import { ChakraProvider } from '@chakra-ui/react'
import { Spinner } from '@chakra-ui/react'

const e = React.createElement;

const searchBtn =  document.getElementById('searchBtn');

const getDomainPath = () => {
  const hiddenInput = document.getElementById('domainPath');
  return hiddenInput.value;
}

const REST_URL = `${getDomainPath()}/wp-json/wp/v1/items`;

const getCategory = () => {
  const form = document.getElementById('cateForm');
  return form.cate.value;
}
const getTag = () => {
  const form = document.getElementById('tagForm');
  return form.tag.value;
}

const Diagnosis = () => {

  const [param,setParam] = useState();
  const [message,setMessage] = useState('');
  const [fetchData,setFetchData] = useState();
  const [isLoading,setIsLoading] = useState(false);

  searchBtn.addEventListener('click', () => {
    const paramCategory = getCategory()
    const paramTag = getTag()

    setParam(`?cate=${paramCategory}&tag=${paramTag}`);
  })

  useEffect(() => {
    setIsLoading(true)

    axios.get(`${REST_URL}${param}`)
      .then(res => {
        console.log(res.data)
        setFetchData(res.data)
        if(res.data.length === 0){
          setMessage('ぴったりのものが見つかりませんでした。')
        }else{
          setMessage('')
        }
        setIsLoading(false)
      })
      .catch(function (error) {
        setIsLoading(false)
        console.log(error);
      })
  },[param])

  return (
    <ChakraProvider>
      {isLoading && (
        <div className="resultItemLoading" >
          <Spinner
            thickness='4px'
            speed='0.65s'
            emptyColor='transparent'
            color='gray.500'
            size='xl'
          />
        </div>
      )}
    <div>
      {fetchData && fetchData.map(item => {
        return(
          <>
            <p>ID:{item.id}</p>
            <p>タイトル:{item.title}</p>
            <hr />
          </>
        )
      })}
      {message &&
        <div>{message}</div>
      }
    </div>
    </ChakraProvider>
  );
}

const domContainer = document.querySelector('#diagnosisArea');
ReactDOM.render(e(Diagnosis), domContainer);

<ポイント>formの値を取得して、エンドポイントのパラメータに入れる

検索ボタンをクリックした時に、page-diagnosis.phpにあるinputタグの値を取得します。
取得した値を、paramというステートを作って格納します。

stateはReactで提供されている機能で、任意の値を入れておけるオブジェクトです。
stateをコンポーネントのレンダー部分(HTMLの部分)に入れておくと、何かの操作でstateの値が変わった時に、その部分のHTMLも変更されます。

クリックした時にsetParamという、paramステートを変更する関数でparamを変更します。

setParam(`?cate=${paramCategory}&tag=${paramTag}`);

すると、useEffect内の処理がはしります。(useStateuseEffectは、ReactのHookという機能ですが、詳細は割愛します)
いよいよエンドポイントを使ってDBからデータをGETしてきます。

<ポイント>axiosでデータをGETする

エンドポイントを使ってデータを取得するために、ここではaxiosを使用します。

axiosのシンプルな使い方は、下記のようになります。

axios.get('/user?ID=12345')
  .then(function (response) {
    // 成功
    console.log(response);
  })
  .catch(function (error) {
    // エラー
    console.log(error);
  })
  .then(function () {
    // always executed
  });

axios.get('エンドポイント')で、functions.phpで返却したものを取得し、responseにそのデータが入ってきます。

今回は、fetchDataというstateをあらかじめ作っておき、取得したデータをsetFetchDatafetchDataにセットしています。
メッセージもstateを作って、データがなかった場合に「ぴったりのものが見つかりませんでした」と表示することにしました。

axios.get(`${REST_URL}${param}`)
  .then(res => {
    console.log(res.data)
    setFetchData(res.data)
    if(res.data.length === 0){
      setMessage('ぴったりのものが見つかりませんでした。')
    }else{
      setMessage('')
    }
    setIsLoading(false)
  })
  .catch(function (error) {
    setIsLoading(false)
    console.log(error);
  })

<おまけ>ローディングアイコンはchakra-uiのSpinnerを使いました。

ローディングアイコンを入れると、よりそれらしくなります。
自前で作ってもよいのですが、簡単にchakra-uiというコンポーネントライブラリを使っています。

import { Spinner } from '@chakra-ui/react'

で、Spinnerをインポートします。
アプリケーションのルートにChakraProviderを設定すると(この辺りの使い方はライブラリの公式通りに)使えるようになります。

import { ChakraProvider } from '@chakra-ui/react'

〜省略〜

<ChakraProvider>
//コンポーネントを使う範囲を囲む
</ChakraProvider>

エラーの記録:Uncaught ReferenceError: process is not defined

esbuildでReactを使おうとして出たエラーです。非常に長い時間ハマりました。

Reactからprocessが呼び出せない…といった内容。いろいろググって試しましたが、どれも解決せず。

  • dotenv追加してみてもだめ
  • env追加してみてもだめ。REACT_APP_をつけないとだめだと書いてあったので、つけてみたけどだめ。(そもそもprocessがよみこめない…)REACT_APP_NODE_ENV=production
  • "react-error-overlay": "6.0.9", を追加してみた。変化なし
  • esbuildでenvを使う必要があるのかと思って、"esbuild-envfile-plugin"を追加してみたけどだめ

上に書いた通り、esbuildのオプションにdefineを追加して、解決しました。

defineは、

This feature provides a way to replace global identifiers with constant expressions. It can be a way to change the behavior some code between builds without changing the code itself:

https://esbuild.github.io/api/#define

訳:この機能は、グローバル識別子を定数表現に置き換える方法を提供します。これは、コードそのものを変更することなく、ビルド間でコードの動作を変更する方法となりえます。

…ということですが、完全に理解はできませんでした。

まとめ

WordPressのRest APIを使うと、書き慣れたコードで、インタラクティブなコンテンツを制作することができます。

ReactやVueを使うと、ページ移動せずに取得したデータを置き換えることができたり、ビルドツールを使うことによってライブラリに付属の便利な機能をimportするだけで使えるようになります。

オリジナルデザインのWebサイトを作っていただけの時は、CSSコンポーネントライブラリの使い道もわからず(逆に不便だと思っていた)、ビルドツールもSassだけのためにを使ってました。
ReactもVueも、Webサイト制作に必要になることは正直ほとんどありませんが、
「使えるようになることを目的に使う」と意識してやり続けた結果、点が線になりそうな気がした案件でした。

おすすめの記事 recommend blog

新着 new blog

github