React + Contentful で「おみくじアプリ」を作ってみる

## はじめに
「Vueはちょくちょく使ってるけど、React触ったことないんだよなぁ」
「Contentful…なんだか今後使う事ありそうだし、使ってみたいなぁ」
そんなフロントエンジニアが、**React + Contentful で "おみくじアプリ"** を作った際の備忘録です。
![5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/1fa3a997-5cc5-ff66-0d84-4adb10ff7d28.gif)

## やる事
- React導入
- Contentful導入
- つなぎ込み
- 実装

## React導入
公式のドキュメント通り

```
npx create-react-app react-omikuji
```
で環境が出来ます。（爆速！最高！）

```
cd react-omikuji
```

で環境に移動して

```
npm start
```

でローカル環境が起動します。
[新しい React アプリを作る - React公式ドキュメント](https://ja.reactjs.org/docs/create-a-new-react-app.html)

## Contentful導入
### サインアップ
[公式ページ](https://www.contentful.com/pricing/)からサインアップします。
freeだとユーザー数やAPIコール数に制限がありますが、一旦freeにて。
実案件で使う際は課金が必須になると思います。

### スペース作成
今回は **omikuji** というIDでスペースを作成します。

### おみくじ結果の登録
情報として以下が必要になります。

- タイトル (例. 大吉)
- 本文 (例. 最高にハッピーな1日になるでしょう)

なので、上記をコンテントモデルに追加します。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/8120d3b0-1650-aa60-7168-67035d7ed180.png)
**Field ID**は

- タイトル => title
- 本文 => description

とします。

続いて、おみくじの結果が複数必要になるので登録していきます。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/2a4b5e70-6927-bfa7-9480-c602d6b55017.png)
このような形で6つ、結果を登録します。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/f06e8169-7095-10a0-63b9-391474d342fe.png)

## つなぎ込み
Contentfulの公式にReactでContentfulを使うチュートリアルがあるので、そちらを参考にReactでContentfulが使えるようつなぎ込みます。
[Getting started with React and Contentful - Contentful公式ドキュメント](https://www.contentful.com/developers/docs/javascript/tutorials/getting-started-with-react-and-contentful/)

### 認証
`https://graphql.contentful.com/content/v1/spaces/[YOUR_SPACE_ID]/explore?access_token=[YOUR_ACCESS_TOKEN]`

上記に自身のスペースIDとアクセストークンを置き換えアクセスします。
するとGraphiQLの画面になるので、GraphQLクエリを以下の形式で記述します。

```
{
  omikujiCollection {
    items {
      title
      description
    }
  }
}
```

エラーがなければ認証完了です。

### ReactでContentfulGraphQLを使用する
App.jsを以下に書き換えます。

```javascript:App.js

import {useState, useEffect} from "react";
import './App.css';
const query = `
{
  omikujiCollection {
    items {
      title
      description
    }
  }
}
`

function App() {
  const [page, setPage] = useState(null);

  useEffect(() => {
    window
      .fetch(`https://graphql.contentful.com/content/v1/spaces/[YOUR_SPACE_ID]/`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer [YOUR_ACCESS_TOKEN]",
        },
        body: JSON.stringify({ query }),
      })
      .then((response) => response.json())
      .then(({ data, errors }) => {
        if (errors) {
          console.error(errors);
        }

        setPage(data.omikujiCollection.items[0]);
      });
  }, []);

  if (!page) {
    return "Loading...";
  }

  return (
    <div className="App">
      <header className="App-header">
        <p>{page.title}</p>
        <p>{page.description}</p>
      </header>
    </div>
  );
}

export default App;
```
これで `npm start` をすれば表示されたはずです。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/e6365985-0216-5606-5cf1-af04eb4bc66d.png)
記述の通り0番目の内容が表示されていると思います。
ここまでエラーがなければつなぎ込みは完了です。

## 実装
### 仕様の確認
まずどのような仕様のおみくじにするのか確認です。

- トップページ( `react-omikuji/` )で「今日の運勢を占う」ボタンを押下すると、 `react-omikuji/result` ページに遷移し、結果が表示される
- ランダムで結果が表示される

上記が問題なければなんでも良しとします。

### ルーティング設定
ページ遷移の挙動を実現するためにreact-router-domをインストールします。

```
npm i react-router-dom
```

### コンポーネント追加
トップページで使う `Button` と、結果ページの `Result` のコンポーネントを用意します。

```javascript:components/Button.js
import React from "react";
import { Link } from 'react-router-dom'

export default class Button extends React.Component {
  render() {
    return (
      <Link to="/result">{this.props.text}</Link>
    )
  }
}
```

```javascript:components/Result.js
import React from "react";

export default class Result extends React.Component {
  render() {
    return (
      <div>
        <p>{this.props.title}</p>
        <p>{this.props.description}</p>
        <a href="/react-omikuji/">トップに戻る</a>
      </div>
    )
  }
}
```

### 実装
App.jsを以下に書き換えます。

```javascript:App.js
import {useState, useEffect} from "react";
import { BrowserRouter as Router, Route } from 'react-router-dom';
import Button from './components/Button';
import Result from './components/Result';
import './App.css';
const query = `
{
  omikujiCollection {
    items {
      title
      description
    }
  }
}
`

function App() {
  const [page, setPage] = useState(null);

  useEffect(() => {
    window
      .fetch(`https://graphql.contentful.com/content/v1/spaces/[YOUR_SPACE_ID]/`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer [YOUR_ACCESS_TOKEN]",
        },
        body: JSON.stringify({ query }),
      })
      .then((response) => response.json())
      .then(({ data, errors }) => {
        if (errors) {
          console.error(errors);
        }
        const number = Math.floor( Math.random() * ((data.omikujiCollection.items.length - 1) + 1 - 0) ) + 0
        setPage(data.omikujiCollection.items[number]);
      });
  }, []);

  if (!page) {
    return "Loading...";
  }

  return (
    <div className="App">
      <Router basename='/react-omikuji/'>
        <Route exact path='/' render={() => <Button text='今日の運勢を占う'/>} />
        <Route exact path='/result' render={() => <Result title={page.title} description={page.description}/>} />
      </Router>
    </div>
  );
}

export default App;
```

### ルートディレクトリ変更
トップページを `react-omikuji/` とするためにpackage.jsonに以下を追記します。

```json:package.json
"homepage": "/react-omikuji/"
```

### 完成
完成です！
![5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116793/1fa3a997-5cc5-ff66-0d84-4adb10ff7d28.gif)
(スタイルは適宜ご調整ください。)

`npm start` でローカル確認が問題なければ、 `npm run build` でビルドされます。

## あとがき
「いやいや、おみくじ判定のタイミング！」
「aタグで戻るんかい！」
などなどありますが、あくまで仕様が


> - トップページ( `react-omikuji/` )で「今日の運勢を占う」ボタンを押下すると、 `react-omikuji/result` ページに遷移し、結果が表示される
> - ランダムで結果が表示される
> 
> 上記が問題なければなんでも良しとします。

という事で…

トークン丸見え問題・APIコール数問題などなどがあるので、ヘッドレスCMS使うならSSG一択なんでしょうかね…

## リンク
[新しい React アプリを作る - React公式ドキュメント](https://ja.reactjs.org/docs/create-a-new-react-app.html)
[Getting started with React and Contentful - Contentful公式ドキュメント](https://www.contentful.com/developers/docs/javascript/tutorials/getting-started-with-react-and-contentful/)