ExcelからElixir入門⑨：ElixirサーバサイドのみでReactと同じSPA／リアルタイムUIが作れる「LiveView」（APIとJavaScriptは書いていない）

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

今回は、[「ExcelからElixir入門④：Webで外部API表示」で作ったQiita検索UI](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109#qiita-api%E3%82%92%E5%91%BC%E3%82%93%E3%81%A7qiita%E3%82%B3%E3%83%A9%E3%83%A0%E6%83%85%E5%A0%B1%E3%82%92web%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)を、LiveViewでリアルタイム検索可能なSPAとして構築し直す手順を解説します

[本コラムは、さまざまな時期のElixir／Phoenix環境で検証済みです](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf#%E5%8F%82%E8%80%83%E6%9C%AC%E3%82%B3%E3%83%A9%E3%83%A0%E3%81%AE%E6%A4%9C%E8%A8%BC%E7%92%B0%E5%A2%83)

**■「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を呼び出す（表示編）【LiveView版】**](https://qiita.com/piacerex/items/64e77b85bfb4c6832662)
|> [**⑦SPAからPhoenix製APIを呼び出す（更新編）【LiveView版】**](https://qiita.com/piacerex/items/78ee0d2ad8cca0927980)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨ElixirサーバサイドのみでReactと同じSPA／リアルタイムUIが作れる「LiveView」**](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)

# LiveView：リアルタイムSPAをサーバサイドのみで開発可

Elixirサーバサイドのみで書かれたコードにも関わらず、Vue.jsやReactのようなリアルタイムSPAが構築できるのが「LiveView」です

Vue.jsやReactとほぼ変わらない反応速度やフィーリングで、しかもフロントとサーバの書き分けが一切要らず、フロント側の入力内容やデータ状態をサーバサイドで一元管理でき、しかもAPI構築要らず … なんだか魔法のような体験ができるLiveViewをお楽しみください :wink:
![chrome-capture-2023-0-13 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/3776529e-e0a0-f0a7-ddbb-a0413c2e82c7.gif)

# LiveView最大の利点：API構築とフロント通信記述が不要

ReactやVue.jsでSPA開発した場合、サーバ側にAPIを構築し、フロント側からaxios等でAPIを呼び出す必要がありますが、これは手間がかかるし、フロント側で状態管理もしなければいけません

更に、APIがすぐに何十本、何百本と増え、管理が煩雑になりやすいのも「SPA開発あるある」です

近年はBFF（Backend For Frontend）でフロントサイドはシンプル化できるものの、サーバサイドはAPIからAPIを呼ぶ構造となるため、性能は劣化しやすく、APIの本数自体が減っている訳ではありません

LiveViewは、API無にフロントとサーバのデータバインディングが可能（しかもリアルタイム）なので、API不要かつフロントからサーバへのAPI呼出処理を書く必要が無く、あくまでElixirサーバサイドのコードだけでSPAを構築できるのがLiveView最大の利点です（BFF構築もElixirサーバサイド内のリファクタリングだけとなります）

# 「Qiita検索SPA」をLiveViewで開発する

それでは早速、以下2バージョンのQiita検索SPAをLiveViewで構築してみましょう

- ①検索キーワード入力されるたびにリアルタイム検索
- ②「検索」ボタン押下時のみ検索（入力確認のみ反映）

その土台となるPhoenix PJ作成／Req導入／Phoenix起動を下記3つで行ってから、この先をお試しください（このシリーズコラムを最初からお試しいただいている方は不要です）

```
mix phx.new basic --no-ecto
cd basic
```

```diff_elixir:mix.exsを編集
defmodule Basic.Mixfile do
  use Mix.Project
…
  defp deps do
    [
+     {:req, "~> 0.4"}, 

      {:phoenix, "~> 1.7.7"},
      …
    ]
  end
…
```

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

なお時間に余裕があって、ElixirやPhoenixを最新化してから試したい方は、下記を先に実施しておいてください（タイトルはUbuntuと入っていますが、MacやWindowsでも大丈夫な手順です）

https://qiita.com/piacerex/items/01e89435af0f7a454ad2#elixir%E3%82%92%E6%9C%80%E6%96%B0%E7%89%882023%E5%B9%B49%E6%9C%88%E7%8F%BE%E5%9C%A8%E3%81%AF1155%E3%81%AB%E4%B8%8A%E3%81%92%E3%82%8B

# ①検索キーワード入力されるたびにリアルタイム検索

最初は、検索キーワードが入力されるたびにQiita APIを呼び出し、リアルタイムで検索した結果を画面にリアルタイム反映する、SPAらしいアプリを作ります

## リアルタイム入力を元にQiita検索を行う

検索フィールドの内容が変わるたびに、サーバから「Req」でQiitaを検索し、その検索結果をフロントに自動反映させます

LiveViewモジュールは、初期時の `mount` と、検索キーワード入力のたびに呼ばれる `handle_event("onChange" ～`、あとQiita APIを呼び出して検索を行う `handle_info({"onSearch" ～` の3つのハンドラを用意します

なお、コード内容は、[LiveViewに関する部分は第6回](https://qiita.com/piacerex/items/64e77b85bfb4c6832662)、[Qiita APIによる検索は第4回](https://qiita.com/piacerex/items/4c212615a4eb699dd109)にて解説済みなので、忘れてしまった方は各リンクを見返してください

```diff_elixir:lib/basic_web/live/qiita_live.ex
defmodule BasicWeb.QiitaLive do
  use BasicWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, [query: "", datas: [], message: "[Init]"])}
  end

  def handle_event("onChange", %{"query" => query}, socket) do
    send(self(), {"onSearch", query})
    {:noreply, assign(socket, [query: query, message: "[Searching...]"])}
  end

  def handle_info({"onSearch", query}, socket) do
    datas = Req.get!("https://qiita.com/api/v2/items?query=#{query}").body
    {:noreply, assign(socket, [datas: datas, message: "[Complete!!]"])}
  end
end
```

下記HTML側は、`<form phx-change="onChange">` で検索キーワードが入力されるたびに `handle_event("onChange", ～` が呼び出すされるようになっており、同時に `Query: <%= @query %>` に検索キーワードを反映するようにしています

ここは、Vue.jsで言うところの `v-on:change` に相当する訳ですが、Vue.jsやReactとの最大の違いは、フロントサイドの関数を呼ぶのでは無く、サーバサイドにあるElixirの関数が呼ばれる点です

```diff_elixir:lib/basic_web/live/qiita_live.html.heex
<%
columns = ["id", "title", "created_at"]
%>
<p>
<%= if @message do %><%= @message %><% end %>
</p>

<form phx-change="onChange">
<input type="text" name="query" value={@query} placeholder="Please input keyword" class="container" /><br>
Query: <%= @query %><br>
</form>

<table class="container">
<tr>
  <%= for column <- columns do %>
  <th><%= column %></th>
  <% end %>
</tr>
<%= for n <- @datas do %>
<tr>
  <%= for column <- columns do %>
  <td><%= n[column] %></td>
  <% end %>
</tr>
<% end %>
</table>
```

## HTMLからElixirサーバサイドの関数を呼び出し、HTMLに返す

`handle_event("onChange", ～` に渡される引数である `query` には、フロント側で入力された検索キーワードがサーバ側に引き渡されます

また、サーバ側の `handle_event("onChange" ～` や `handle_info({"onSearch" ～` で行われる `message` の更新は、フロント上のメッセージ領域に反映されます

更に、`handle_event("onChange", ～` からメッセージパッシングである `send` で `handle_info({"onSearch" ～` が呼び出され、`handle_event("onChange", ～` はその直後に `message: "[Searching...]"` でメッセージ領域を「[Searching...]」に更新し、その後、Qiita API呼出で検索された結果と、`message: "[Complete!!]"` を返すことで、メッセージ領域を「[Complete!!]」に更新しつつ、検索結果がHTML側の `<%= for n <- datas do %>` で始まる検索結果表示に反映されます

## ルーティング追加

さて、SPAが実装できたので、後はルーティングを設定したら完成です

```diff_elixir:lib/basic_web/router.ex
defmodule BasicWeb.Router do
  use BasicWeb, :router
…
  scope "/", BasicWeb do
    pipe_through :browser

    live "/", IndexLive
+   live "/qiita", QiitaLive
…
```

## ①の動作確認

ブラウザで「``http://localhost:4000/qiita``」にアクセスすると、こんな感じのQiita検索SPA画面が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6bb5d6c8-cd75-fc58-5b50-f2abc70c1fcf.png)

検索キーワードを入力すると、`Query:` へと入力した文字列がリアルタイム反映され、上部には「[searching...]」と検索中であるメッセージが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/10e54c5a-167b-38b3-da88-f670f0a0477c.png)

その裏では、Qiitaへの検索が走っており、少しすると、メッセージが「[complete!!]」に変わり、画面下部に検索結果が反映されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/57bf2b6f-4409-37ff-cb01-5eaf3b514c48.png)

# ②「検索」ボタン押下時のみ検索（入力確認は即時反映）

業務系のWebアプリでは、入力都度に検索を走らせず、サブミットボタン押下時のみ検索を走らせるケースが多いと思いますが、LiveViewでその実装を叶えてみましょう

まずHTMLで、`<from>` に `phx-submit` を追加して、「検索」ボタンも追加することで、サブミット時の検索を可能にします

```diff_elixir:lib/basic_web/live/qiita_live.html.heex
<%
columns = ["id", "title", "created_at"]
%>
<p>
<%= if @message do %><%= @message %><% end %>
</p>

-<form phx-change="onChange">
+<form phx-change="onChange" phx-submit="onSearch">
<input type="text" name="query" value={@query} placeholder="Please input keyword" class="container" /><br>
Query: <%= @query %><br>
+<input type="submit" value="検索" class="bg-blue-500 text-white py-2 px-4">
</form>

<table class="container">
<tr>
  <%= for column <- columns do %>
  <th><%= column %></th>
  <% end %>
</tr>
<%= for n <- @datas do %>
<tr>
  <%= for column <- columns do %>
  <td><%= n[column] %></td>
  <% end %>
</tr>
<% end %>
</table>
```

LiveViewモジュールは、`handle_event("onChange", ～` での `send` をしないことでQiita検索を止め、 `handle_info({"onSearch", ～` を `handle_event("onSearch", ～` に書き換えることで、サブミット時にQiita検索するように変更します

```diff_elixir:lib/basic_web/live/qiita_live.ex
defmodule BasicWeb.QiitaLive do
  use BasicWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, [query: "", datas: [], message: "[Init]"])}
  end

  def handle_event("onChange", %{"query" => query}, socket) do
-   send(self(), {"onSearch", query})
-   {:noreply, assign(socket, [query: query, message: "[Searching...]"])}
+   {:noreply, assign(socket, [query: query, message: ""])}
  end

- def handle_info({"onSearch", query}, socket) do
+ def handle_event("onSearch", %{"query" => query}, socket) do
    datas = Req.get!("https://qiita.com/api/v2/items?query=#{query}").body
-   {:noreply, assign(socket, [datas: datas, message: "[Complete!!]"])}
+   {:noreply, assign(socket, [query: query, datas: datas, message: "[Complete!!]"])}
  end
end
```

## ②の動作確認

検索キーワードを入力しても、Qiita検索は走らず、上部のメッセージは消え、入力したキーワードの `Query:` へのリアルタイム反映だけが行われます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f818dbf7-1ef7-e2c6-1ba9-ad35de045bae.png)

「検索」ボタンをクリックしたときだけ、Qiitaへの検索が走り、少しすると、メッセージが「[complete!!]」に変わり、画面下部に検索結果が反映されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/208b92fe-f7a5-5dc4-ac7d-0088211014be.png)

LiveViewの反応速度はスピーディで、もたつく感じは一切ありません（ReactやVue.jsでフロントを書いているフィーリングと、ほぼ変わらないでしょう）

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

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

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

# 終わり

LiveViewによって、Elixirサーバサイドのコードを書くだけで、フロントUIのリアルタイム操作／反映が叶いました

LiveViewが、SPAを作るための強力過ぎる土台だという感触が伝わったでしょうか？

[次回は、今回作成した **LiveView SPAアプリをGigalixirにリリース** します](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)