REST APIを利用し、WordPressのフロントエンドをReactで構築する

#まえがき 2020.6.11投稿
WordPressの管理画面で
記事を投稿したりコンテンツを編集する機能はクライアント向けに残しておきたい。
しかしフロントエンドはモダンjsフレームワークであるReactを利用しSPAとしたい。
これが本記事の目的です。

#この記事でできるようになること
・WordPressのフロントエンドを、Reactで組めるようになる
・WP REST APIをReactで使えるようになる
・カスタムフィールドの内容をReactでどう取得して出力するかわかる
・Reactでタブコンテンツを実装できるようになる
・Reactでハンバーガーメニューを実装できる
・json形式のデータをmapで出力できる

ハンバーガーメニューはReactでclass名をtoggleしています。
cssは適宜用意してください。
タブコンテンツも同様です。
※参考[ハンバーガーボタン](https://www.nxworld.net/tips/12-css-hamburger-menu-active-effect.html)
※参考[タブ](https://codepen.io/jamesreegan/pen/ZYNrBa?__cf_chl_jschl_tk__=19b2019ff26b45b04ecd06556098af741e72872b-1591876327-0-ATJcfie7eVmuIeY3Cdv6T-eNPOIN4Tws_1tmwwTzfNE-ACwp72WCEVjJo_CRXt3711SwMdfuKl9RPZP66Zlj06WDZfoWEab7Byf_LWadfQndUUypr_aJn5Opuc66W548e1w7JXOuyzA6i6Q8a3DFlZ8w850LsgcHrvgP5AgVVgnWZO3bSBUV4jugRh6Qm8hXfmEdg7ARDaNv48DMnUI6fFb7yS-2BwwLCm5MF3NkHabk2OXfjUSYRstMJhIaRVZP1ElX6vAYsPf9gINcq3CDHp7J5lhm6aLUrdzWhU5KPkUFZgTZ52-m0LzMA9jsZ1gyiJXHFfREhSFBGb99fSr-3KWIP9bMxXjShjOUh91GuqL6)


##まずWordPress
URLを最終的に
https://example.com/
でReact公開する場合
https://example.com/wp/
などにWordPressを設置してください。
設置方法は特に説明しません。
Advanced custom fieldsでカスタムフィールドを設定しておく。

##Reactプロジェクトを作成

```
$ create-react-app your-project-name
```

##react-router-domのインストール

```
$ npm install react-router-dom
```

##各ファイルの編集
まず./src/index.js


```javascript:index.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';

import './style.css';
import * as serviceWorker from './serviceWorker';



const site_url = 'https://example.com/wp/';//ここにWordPress本体のURLを記述
const posts_url = site_url + 'wp-json/wp/v2/posts/?_embed';//ただの投稿一覧を取得する場合のREST API記述
const pages_url = site_url + 'wp-json/wp/v2/pages/3?_embed';//固定ページのID3を取得する場合
const pages_url2 = site_url + 'wp-json/wp/v2/posts/146?_embed';//投稿ID146を取得する場合
const listurl = site_url + 'wp-json/wp/v2/posts?categories=2&_embed';//categoryID2の投稿一覧を取得する場合
//_embedのパラメータを付与するのは、記事に設定されているサムネイル画像を取得するため

//ディスクリプションを取得
let description = document.getElementsByName('description')[0].content;
console.log(description);//取得したディスクリプションをconsoleで確認

//ヘッダコンポーネントを定義
class Header extends React.Component {
  render(){
    return (
      <header>
        <div>
          <h1><Link to='/'><img src="/img/logo.png" alt="ロゴ" /></Link></h1>
          <ul>
            <li><Link to='/'>HOME</Link></li>
            <li><Link to='/about/'>About</Link></li>
            <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
          </ul>
          <div id="menu">
            <App/>
          </div>
        </div>
      </header>
    );
  }
}

//パンくず
class Bread extends React.Component {
  render(){
    return (
      <div>
        <ul className="bread">
          <li><Link to='/'>HOME</Link></li>
          <li>{document.title}</li>
        </ul>
      </div>
    );
  }
}

//サイドバーコンポーネント
class Sidecat extends React.Component {
  render(){
    return (
      <div id="sidecat">
        <h2>カテゴリ別</h2>
        <ul>
          <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
        </ul>
      </div>
    );
  }
}

//スマホ表示時のハンバーガーメニュー用コンポーネント
class App extends React.Component {
  //this.state.isShowを宣言
  constructor(props) {
    super(props);
    this.state = {
      isShow : false
    }
    //toggleをbindしておかないと動かない(たぶん)
    this.toggle = this.toggle.bind(this)
  }
  //toggleメソッドを定義
  toggle(e) {
    this.setState({
      isShow: !this.state.isShow 
    });
  }
  render() {
    return (
      <div>
        <a className={'menu-trigger' + (this.state.isShow ? ' active' : '')}  onClick={this.toggle}>
          <span></span>
          <span></span>
          <span></span>
        </a>
        <ul className = {'menu' + (this.state.isShow ? ' active' : '')}>
          <li><Link to='/'>HOME</Link></li>
          <li><Link to='/about/'>About</Link></li>
          <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
        </ul>
      </div>
    );
  }
}

//複数行テキストの改行を保持するためのコンポーネント [出典](http://qs.nndo.jp/display-multiline-text-by-react)
//カスタムフィールドで改行含むコンテンツを呼び出すとき、<MultiLineText></MultiLineText>で囲む
class MultiLineText extends React.Component {
  render() {
    const renderTexts = () => {
      if (typeof(this.props.children) === "string") {
        return this.props.children.split("\n").map((m,i) => <span key={i}>{m}<br/></span>)
      } else {
        return "";
      }
    }
    return (
      <div className={this.props.className}>
        {renderTexts()}
      </div>
    );
  }
}

//indexページのコンポーネント
class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: 'トップページ',
    };
  }
  componentDidMount(){
    document.title = 'トップページ';//titleタグ書き換え
    document.getElementsByName('description')[0].content = 'トップページのディスクリプション';//descriptionタグ書き換え
  }
  render() {
    return (
      <div>
        <Header/>
        <div id="main">
          <!--indexページの内容-->
        </div>
      </div>
    );
  }
}

//固定ページAboutのコンポーネント
class About extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
    };
  }

  componentDidMount(){
    document.getElementsByName('description')[0].content = 'Aboutのディスクリプション';
    document.title = 'Aboutのタイトル';
     fetch(pages_url)//pages_urlで定義した固定ページからpromiseでデータを取得
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });
    
     })
     .catch((error) =>{
       console.error(error);
     });
  }

  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);
    //data.acf.body1で、カスタムフィールドbody1の内容を取得し出力できる
    if(this.state.loading){
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="post">
              　　<h2>{data.title.rendered}</h2>
              　　<p>{data.acf.body1}</p>
              　　<MultiLineText>{data.acf.body2}</MultiLineText>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}

//個別記事ページ用コンポーネント（タブ機能つき）
class Post extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      isShow : false,
      active1: 'active',
      active2: '',
      active3: '',
    }
    //toggle用にbind
    this.toggle1 = this.toggle1.bind(this);
    this.toggle2 = this.toggle2.bind(this);
    this.toggle3 = this.toggle3.bind(this);
  }
  //toggle1でtab1をアクティブに
  toggle1(e) {
    this.setState({active1:'active'});
    this.setState({active2:''});
    this.setState({active3:''});
  }
  //toggle2でtab2をアクティブに
  toggle2(e) {
    this.setState({active1:''});
    this.setState({active2:'active'});
    this.setState({active3:''});
  }
  //toggle3でtab3をアクティブに
  toggle3(e) {
    this.setState({active1:''});
    this.setState({active2:''});
    this.setState({active3:'active'});
  }
  componentDidMount(){
     //posts_urlに、URLパラメータで送られてきた投稿IDをくっつけてpromiseで投稿記事のデータを取得
     fetch(post_url+this.props.match.params.id+'?_embed')
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });
    
     })
     .catch((error) =>{
       console.error(error);
     });
  }
  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);
    //data.acf.rubyでカスタムフィールドrubyの値を取得 例外として、件名、本文など一部フィールドはdata.title.renderedで取得
    if(this.state.loading){
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="post">
                <h2>{data.title.rendered}<span>{data.acf.ruby}</span></h2>
                  <nav>
                    <a href='javascript:void(0);' data-tab='one' onClick={this.toggle1} className={(this.state.active1 )}>TAB1</a>
                    <a href='javascript:void(0);' data-tab='two' onClick={this.toggle2} className={(this.state.active2 )}>TAB2</a>
                    <a href='javascript:void(0);' data-tab='three' onClick={this.toggle3} className={(this.state.active3 )}>TAB3</a>
                    <div className='clear'></div>
                  </nav>            
                  <div className="tabwrap">
                    <div className='tabContainer'>
                      <div id='one' className={('Tabcondent ' + this.state.active1 )}>
                        <!--tab1のコンテンツ-->
                      </div>
                      <div id='two' className={('Tabcondent ' + this.state.active2)}>
                        <!--tab2のコンテンツ-->
                      </div>
                      <div id='three' className={('Tabcondent ' + this.state.active3)}>
                        <!--tab3のコンテンツ-->
                      </div>
                    </div>
                  </div>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}

//投稿記事の一覧コンポーネント
class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false
    };
  }

  componentDidMount(){
    document.getElementsByName('description')[0].content = 'このコンポーネント用ディスクリプション';
    document.title = 'このコンポーネント用タイトルタグ';
     fetch(list_url)
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });
    
     })
     .catch((error) =>{
       console.error(error);
     });
  }
  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);
    
    //一覧に含まれる記事の数だけmapでまわす
    if(this.state.loading){
      const side = data.map((value,i) => (
        <li key={i}><Link to={'/post/' + value.id}>{value.title.rendered}</Link></li>
      ))
      const list = data.map((value,i) => (
        <div key={i}>
          <img className='list_image' src={value.acf.list_image}/>
          <Link to={'/post/' + value.id}>詳細を見る</Link>
        </div>
      ))
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="main_content">{list}</div>
              <div id="side">
                <Sidecat />
                <ul id="sidemenu">{side}</ul>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}




ReactDOM.render((
  <BrowserRouter>
    <Switch>
      <Route path='/' exact component={Index}/>
      <Route path='/about/' component={About}/>
      <Route path='/list/' component={List}/>
      <Route path='/post/:id' component={Post}/>
      </Switch>
  </BrowserRouter>
  ), document.getElementById('root')
);


```
##index.htmlの内容
./public/index.html

```HTML:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="デフォルトのディスクリプション">
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <script src="https://kit.fontawesome.com/409eaacc50.js" crossorigin="anonymous"></script>
    <title>デフォルトのタイトル</title>
    <base href="/">
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="content"></div>
    <footer>
    Copyright © 
    </footer>
    
  </body>
</html>

```

##style.cssは好きなように
./src/style.css



##最後に

以上で実装終わりです。
ウェブサーバに公開する場合は
`yarn build` または `npm run build`して
buildの中身をアップロードすればいけると思います。

自分はこれをやるためにあれこれ検索してまわり、
「できる」ということはわかっていたのですが
具体的に細かく自分のレベルで理解できるほどの記事を見つけることができず、
いろいろな記事や情報を調べつつ、
メンターの方を探して質問させていただいたりしつつ、やっと完成したので
作り方を残したいと思います。