ExcelからElixir入門⑥：SPAからPhoenix製APIを呼び出す（表示編）【React版】

【本コラムは、5分で読めて、15分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex)です、ご覧いただいてありがとございます :bow:

[前回](https://qiita.com/piacerex/items/290b76b76d5ff8e019bf)までは、シンプルなPhoenixのサーバサイドレンダリング（SSR）により、Web上にDBデータや外部APIデータを表示してきました

今回からは、Reactを使ったフロントサイドと、Phoenixによる内部APIを組み合わせを用いて、よりElixir／Phoenixらしさを実感できるWeb開発へとステージアップします

なお、本コラムと全く同じ内容のLiveView版とVue.js版もあります

**LiveView版**
https://qiita.com/piacerex/items/64e77b85bfb4c6832662

**Vue.js版**
https://qiita.com/piacerex/items/50d847170291c41fef64

**■「ExcelからElixir入門」シリーズの目次**
[**①データ並替え／絞り込み**](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1)
|> [**②データ列抽出、Web表示**](https://qiita.com/piacere_ex/items/b7787580fce5f148242f)
|> [<b>③WebにDBデータ表示</b>](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6)
|> [<b>④Webに外部APIデータ表示</b>](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109)
|> [<b>⑤Webにグラフ表示</b>](https://qiita.com/piacere_ex/items/290b76b76d5ff8e019bf)
|> [**⑥SPAからPhoenix製APIを呼び出す（表示編）【React版】**](https://qiita.com/piacerex/items/27052ac628ec394c7560)
|> **⑦SPAからPhoenix製APIを呼び出す（更新編）【React版】**
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨「LiveView」ElixirサーバサイドのみでReact的SPA／リアルタイムUIが作れる**](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
|> [**⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース**](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)
|> [**⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開**](https://qiita.com/piacerex/items/b61348c282395167fd8c)
|> [**⑫Elixir／PhoenixのCRUD Webアプリをリリース**](https://qiita.com/piacerex/items/511f24888c7350639d0b)

<h3>:ocean::ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位！ :ocean::ocean::ocean:</h3>

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ！ :qiita: :tada: :confetti_ball:

https://qiita.com/advent-calendar/2022/elixir
https://qiita.com/advent-calendar/2022/ranking/feedbacks
https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/356a15c6-0dbb-097e-38f3-a1b4fed47555.png)](https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages)

# PhoenixでScaffoldを使わずにデータ一覧APIを作る

通常、PhoenixでAPIを作る場合、`mix phx.gen.json` によるDB CRUD付きREST API、いわゆる「Scaffold」で生成するのが一般的ですが、別に使わなくてもPhoenixでのAPI作成はカンタンですので、早速作ってみましょう

まず 「Sqlex」をインストールすることで、`DbMnesia`／`Db` モジュールを導入します（このシリーズの第3回まではこの2つの実装も扱っていましたが、入門編を超えた範囲になるのでOSS化しました）

https://github.com/piacerex/sqlex

mix.exsの `def deps do` 配下の `:phoenix` の直上に追記します

```diff_elixir:mix.exs
defmodule Basic.Mixfile do
  use Mix.Project
…
  defp deps do
    [
      {:req, "~> 0.3"}, 
+     {:sqlex, "~> 0.1.0"}, 
      {:phoenix, "~> 1.6.15"},
      …
    ]
  end
…
```

PhoenixをCtrl+C2回で止めて、ライブラリを取得（要ネット接続）し、Phoenixを起動します

```sh
mix deps.get
iex -S mix phx.server
```

次に、`controllers` フォルダ配下に `member_controller.ex` というファイルを追加し、下記のように `index` で `members` テーブルの一覧を返すようにします

Phoenixは、マップやリスト、リストマップを `json` に渡すと、自動的にJSON化してくれるのです

前回までで、DB（Mnesia）を使うPhoenix PJを作りましたが、そこに下記ファイルを追加しましょう

```diff_elixir:lib/basic_web/controllers/member_controller.ex
defmodule BasicWeb.MemberController do
  use BasicWeb, :controller

  def index(conn, _p) do
    conn
    |> json(
      "select * from members"
      |> Db.query
      |> Db.columns_rows 
      |> Enum.sort(fn current, next -> current["id"] < next["id"] end)
    )
  end
end
```

次に、ルーティングにAPI用エントリーとして、CRUD全部（GET／POST／PUT／DELETE）のエンドポイントを一括生成する `resources "/", MemberController` を含む `scope "/members", ～` ブロックを追加します

```diff_elixir:lib/vue_sample_web/router.ex
defmodule BasicWeb.Router do
  use BasicWeb, :router
…
+ scope "/members", BasicWeb do
+   pipe_through :api
+ 
+   resources "/", MemberController
+ end
  …
```

Phoenixを起動してください

```
iex -S mix phx.server
```

`http://localhost:4000/members` にブラウザでアクセスすると、下記のように返ってくれば成功です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7937903d-fe0a-c3bd-2c77-688123253470.png)

## データ一覧APIをREST APIクライアントで直接叩いて確認

REST APIの確認には、「REST APIクライアント」が便利ですが、REST APIクライアントは下記が代表的です

**Chrome向け「Postman」**

https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop

**Firefox向け「RESTClient」**

https://addons.mozilla.org/ja/firefox/addon/restclient/

**VScode向け「REST Client」**

https://marketplace.visualstudio.com/items?itemName=humao.rest-client

ここではPostmanで説明しますが、下記のように「Headers」に `Content-Type` を `application/json` で設定してから、URLを設定し、「Send」ボタンをクリックしてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/abc5f278-3e4f-d1ee-2886-ae1d2430fc24.png)

すると、下記のように返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/28e62dad-84a2-25b6-152e-7c60017f2cf3.png)

このように、PhoenixでAPIを作成するのは、とてもカンタンです

# SPAからデータ一覧APIを呼んだ後、表示する

Reactとaxiosを使って、先ほど作ったAPIを呼ぶSPA（Single Page Application）を作ってみましょう

Reactとaxiosは、`<script src=～>` でロードします（「CDN（Content Delivery Network）」という形式です）

`App` 関数が、Reactのメイン処理です

`useEffect` ハンドラから呼び出される `get` で初期表示時のaxiosによるAPI呼出を行い、`React.useReducer` で定義された `members` に `dispatch` 関数経由でAPIで取得したデータを格納します

それを  `members.map` で1件ずつ取り出し、各フィールドを `{n.【フィールド名】}` で取り出して表示します

なお、`useEffect` ハンドラの第2引数に `[]` を指定すると、ロード時に1度のみの実行となります（Vue.jsの `mounted` と同じような挙動）

```html:lib/basic_web/templates/page/index.html.heex
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material/umd/material-ui.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 

<script type="text/babel">
const App = () => {
  const reducer = (members, action) => {
    return action
  }
  const [members, dispatch] = React.useReducer(reducer, [])
  React.useEffect(() => {get()}, [])
  const get = async () => {
    await axios.get('/members').then(response => dispatch(response.data))
  }
  return (
    <div>
    <h1>Members</h1>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>team</th>
        <th>position</th>
      </tr>
      {members.map((n) => (
      <tr>
        <td>{n.id}</td>
        <td>{n.name}</td>
        <td>{n.age}</td>
        <td>{n.team}</td>
        <td>{n.position}</td>
      </tr>
      ))}
    </table>
    </div>
  )
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>

<div id="root"></div>
```

なお、useStateで書くと以下の通りです

```html:lib/basic_web/templates/page/index.html.heex
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material/umd/material-ui.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 

<script type="text/babel">
const App = () => {
  const [members, setMembers] = React.useState([])
  React.useEffect(() => {get()}, [])
  const get = async () => {
    await axios.get('/members').then(response => setMembers(response.data))
  }
  return (
    <div>
    <h1>Members</h1>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>team</th>
        <th>position</th>
      </tr>
      {members.map((n) => (
      <tr>
        <td>{n.id}</td>
        <td>{n.name}</td>
        <td>{n.age}</td>
        <td>{n.team}</td>
        <td>{n.position}</td>
      </tr>
      ))}
    </table>
    </div>
  )
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>

<div id="root"></div>
```

APIで取得したデータがテーブル表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6315519d-98d6-c6f5-556f-efea315c4328.png)

見た目が、SSR版と変わらないので、ちゃんとSPAとして動いているか分からないかも知れませんが、ブラウザの「開発者モード」の「Network」で確認すると、APIが呼ばれていることが分かります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/0cdb3983-af3a-060e-5c47-a74157e042eb.png)

Phoenixを起動したコンソールでも下記の通り、API呼出である `MemberController.index` の実行が、ページ呼出である `PageController.index` の実行の後に行われていることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/08f10e87-0d8d-278f-ee38-2bab2312abb4.png)

# データ追加APIを実装する

APIによるデータ参照はできたので、次は、データ追加APIを実装してみます

`MemberController` モジュールに `create` を追加し、`members` テーブルへのinsert文を `Db.query` に渡し、その後、ステータスコードとして `201 Created` を返却します

```diff_elixir:lib/basic_web/controllers/member_controller.ex
defmodule BasicWeb.MemberController do
  use BasicWeb, :controller

  def index(conn, _p) do
    conn
    |> json(
      "select * from members"
      |> Db.query
      |> Db.columns_rows 
      |> Enum.sort(fn current, next -> current["id"] < next["id"] end)
    )
  end
+ def create(conn, p) do
+   "insert into members values('#{p["name"]}', #{p["age"]}, '#{p["team"]}', '#{p["position"]}')"
+   |> Db.query
+   send_resp(conn, :created, "")
+ end
end
```

## データ追加APIから直接データを追加し、Web上で確認

では、APIでデータ追加してみましょう

「GET」を「POST」に変更し、「Body」を「raw」で設定した後、下記内容をbodyに入力して、藤井名人をチームにjoinしてもらい、大幅戦力増強しましょうｗ

```json
{
  "name": "藤井 聡太", 
  "age": 20, 
  "team": "杉本昌隆八段門下", 
  "position": "竜王"
}
```

入力後は、こんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6b58bfce-70e8-d504-3a74-c6ae6ca775f4.png)

「Send」ボタンをクリックすると、「Status」に `201 Created` が返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/200ba380-ece1-ae8e-1c67-82b1494052c2.png)

データ一覧APIを叩くと、藤井名人が追加されているのが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7471791e-f6e8-8ff6-d935-d9bcb002d372.png)

ブラウザをリロードすると、藤井名人がチームにjoinしたことが確認できました :wink:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/cbdd26eb-88d7-6b39-788d-6a32c441da43.png)

# 【参考】本コラムの検証環境

本コラムは、以下環境で検証しています（恐らくUbuntu実機やMacでも動きます）

- Windows 10
  - 実機＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15
  - WSL2／Ubuntu 20.04＋Elixir 1.14.2 (Erlang/OTP 25)　※[**最新版のインストール手順はコチラ**](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)
    - Phoenix 1.6.15
  - Docker／Debian 11.6＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15

# 終わり

今回は、Reactによるフロントサイドと、Phoenixによる内部APIを組み合わせを用いて、APIで取得したデータをWeb表示してみました

次回は、**画面入力と、APIによるデータ更新** を行ってみます