Wordpressの一部にReactを組み込む方法

# 概要
Wordpressで作成していた社内ポータルサイトで、ページ一覧を既存のプラグインでは希望のデザインが実現できなかったため、Wordpress Rest APIとReactで実現した。

# 実現方法
## Reactの作成
create-react-appでReactアプリを作成する。
次に、App.tsxにWordpressに組み込みたい部品Articlesを入れる。

```App.tsx
import React from 'react';
import './App.css';

import Articles from './Articles';

function App() {
  return (
    <div className="App">
      <Articles />
    </div>
  );
}

export default App;
```

次に、部品本体のArticlesを作成する。

Wordpress Rest APIからAxiosで記事を取得してきて、一覧表示しています。
途中までサンプルを作成し、後は他のメンバーに開発して貰ったので、ページネーションの実装は適当です。

```App.tsx
import React, { useState, useEffect } from "react";
import { Theme, createStyles, makeStyles } from '@material-ui/core';

import { format, getUnixTime, parse, parseISO, toDate } from 'date-fns';
import { ja } from 'date-fns/locale'

import axios from 'axios';

import {
  Grid,
  CircularProgress,
  Tabs,
  Tab,
  Card,
  CardActionArea,
  CardHeader,
  CardMedia,
  CardContent,
  CardActions,
  Button,
  Chip
} from '@material-ui/core';

// FetchAPIでのデータ取得先(Wordpressのトップ)
const base_url = "https://hogehoge/"

const time_format = 'M月d日(EEE) HH時HH分';
const locale = { locale: ja };


const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      margin: theme.spacing(1),
    },
    tab: {
      margin: theme.spacing(1),
      textAlign: "center",
    },
    card: {
      "& *": {
        textDecoration: "none",
      }
    },
    itemMessage: {
      width: 'auto',
      fontSize: "14px",
      lineHeight: "21px",
      maxHeight: "61.6px",
      overflow: "hidden",
      textOverflow: "ellipsis",
      whiteSpace: "nowrap",
      display: "-webkit-box",
      "-webkit-box-orient": "vertical",
      "-webkit-line-clamp": "3",
  },
  }),
);

type Articles = {
  total: number,
  data: any[]
};

type Props = {};

export const Articles = (props: Props) => {

  const [data, setData] = useState<Articles>({total:0, data: []});
  const [loading, setLoading] = useState(true);
  const [select_tab, setTab] = useState(0);
  const [page, setPage] = useState(1);

  const classes = useStyles(props);

  // 辞書オブジェクトを作成する
  const categories_dic: { [name: number]: string } = {
    10: "news",
    11: "event",
    12: "column",
  };

  // useEffect
  useEffect(() => {
    console.log('useEffect');

    let cleanedUp = false;

    const fetchData = async () => {
      console.log("fetchData")

      const per_page = 8;
      const all_query = "/wp-json/wp/v2/posts?per_page="+ per_page;

      var query = "";
      
      if (select_tab == 0){
        query = base_url + all_query;
      } else if (select_tab == 1){ // news
        query = base_url + all_query + "&categories=10";
      } else if (select_tab == 2){ // event
        query = base_url + all_query + "&categories=11";       
      } else if (select_tab == 3){ // column
        query = base_url + all_query + "&categories=12";     
      }

      if (page > 1){
        query += "&offset=" + per_page;
      }
      //最初の10件を取得
      const response = await axios.get(query)
      //ヘッダから総記事数を取得
      const total = response.headers['x-wp-total']
      const result = response.data;

      console.log(total);           
      console.log(result);
      if (!cleanedUp) { // unmount後にデータの読み込みが完了した場合はデータを設定しない
        setData({total:total, data: result});

        setLoading(false);
      }
    };

    fetchData();

    return () => { cleanedUp = true; }; 
  }, [setData, select_tab, page]);

  // タブをクリック
  const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
    setPage(1);
    setTab(newValue);

  };

  // ページネーションDownクリック
  const onPagenateDown = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    console.log("onPagenateDown click")
    setPage(page - 1);
  };

  // ページネーションUpクリック
  const onPagenateUp = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    console.log("onPagenateUp click")
    setPage(page + 1);
  };

  // for pagination
  const disabled = false;

  if (loading || data === undefined) {
    console.log("Loadingの表示");
    return (<div><CircularProgress></CircularProgress></div>)

  } else {

    const list = data.data ?? [];

    return (
      <>
        <Tabs value={select_tab} onChange={handleChange} className={classes.tab} >
          <Tab label="ALL" />
          <Tab label="NEWS" />
          <Tab label="EVENT" />
          <Tab label="COLUMN" />
        </Tabs>
        <Grid container className={classes.root} spacing={3} justify="center" >
          {list
            .map((article: any) => (
              (
                <Grid item xs={4} key={article.id}>
                  <div>
                    <Card className={classes.card}>
                      <a href={article.link}>
                        <CardActionArea>
                          <CardHeader
                            avatar={
                              <Chip
                              label={categories_dic[article.categories]}
                              clickable
                              color="primary"
                            />
                            }
                            title={decodeURI(article.title.rendered)}
                            subheader={"投稿日:" + format(parseISO(article.date), time_format, locale)}
                            
                          />
                          <CardMedia
                            className={classes.tab}
                            image="hoge"
                            title="fuga"
                          />
                          <CardContent className={classes.itemMessage}>
                            {article.content.rendered},
                          </CardContent>
                        </CardActionArea>
                      </a>
                      <CardActions>
                      </CardActions>
                    </Card>

                  </div>
                </Grid>

              )
            ))}
        </Grid>
        <div className="pagination">
            <button
                className="pagination__button pagination__button--prev"
                type="button"
                disabled={page <= 1}
                onClick={onPagenateDown}
            >
                前へ</button>
            <button
                className="pagination__button pagination__button--next"
                type="button"
                disabled={page >= data.total}
                onClick={onPagenateUp}
            >
                次へ</button>
        </div>
      </>
    );

  }

}

export default Articles;
```

## Wordpressに掲載する
Reactをビルドして、build/static/配下にあるファイルをWebサーバにコピーする。

次に、build/index.htmlのファイルを参照し、```<div id="root"></div>```から```<script src="/static/js/main.10cf41a7.chunk.js"></script>```をメモし、Wordpressの本パーツを利用したい箇所に埋め込む。

```
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
    <link href="/static/css/main.a617e044.chunk.css" rel="stylesheet">
</head>

<body><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script>!function (e) { function r(r) { for (var n, a, i = r[0], c = ★省略★</script>
    <script src="/static/js/2.bce676ce.chunk.js"></script>
    <script src="/static/js/main.10cf41a7.chunk.js"></script>
</body>

</html>
```

## パーツの動作イメージ
Wordpressの記事の一覧をWordpress Rest APIで取得して、一覧にしています。
タブでカテゴリーを選択可能としています。

![WordpressRestAPIwithReact.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293373/cfde5c01-76c2-4f1b-0399-7ea96f0f9c7d.png)

# 終わりに
Wordpressのプラグインは便利ですが、UI関連は微妙に要件と合致しないことが多く、Reactで作ってしまった方が早く、メンテもしやすいと思います。