さようならAPI! InertiaでLaravelのformを書こうの巻

こちらは [スタジオ・アルカナ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/studio_arcana) の12日めの記事になります！

# はじめに

前回の記事では Inertia を使ってBladeを書こう！というお話をしました。

https://qiita.com/sa-gimayama/private/374830b89bf26ac2f9f5

そこで `Formを利用したデータのやり取り` について書くと予告して終わりました。

LaravelでInertiaを使わずともじつはReactやVueJSを使うことはできます。できるのですが、Inertiaを使うと色々楽できるということなのです。
そのうちの一つがフォームを使ったデータのやり取りになります。

今回は例として「食べたおやつのカロリーをただ足し合わせて算出する」サービスを作ったとします。
セレクトボックスから選べるおやつは「閉鎖したアルカナ朝霞オフィス（旧アンテクオフィス）周辺で夕方によく買ってたおやつ」です。
新宿になってからはおやつはオフィス内でも買えるようになったので外にあまり買いに行かなくなりました。。。
外に買いに行っても良いんですけど、なまじ新宿になってからその気になれば何でも買えるってなると逆に絞れなくなりますよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882388/976e81a8-176f-4fe0-11c0-2f4c473b577d.png)

今回使う例はGitHubに置いてあるので、参考にしてください。
https://github.com/sa-gimayama/example2024

# Inertiaじゃない場合（普通のblade）

Inertiaじゃない場合、普通にフォームリクエストになります。 `<form>` タグ使うあれですね。
普通のHTMLなら普通にフォームからpostすればいいのでこれが当然一番簡単になりますよね。

```php:example.ateOyatsu.blade.php
<head>

</head>
<body>
<h1>おやつを食べる</h1>
<form action="{{route('example.ateOyatsu.blade.update')}}" method="post">
    @csrf
    <select name="oyatsu_id">
        @foreach($oyatsus as $oyatsu)
            <option value="{{ $oyatsu->id }}">{{ $oyatsu->name }}({{ $oyatsu->calory }}kcal)</option>
        @endforeach
    </select>
    <input type="date" name="ate_at">
    <button type="submit">送信</button>
</form>
<hr>
<h2>食べたおやつ</h2>
<table>
    <tr>
        <th>名前</th>
        <th>カロリー</th>
        <th>食べた日</th>
    </tr>
    @foreach($ateOyatsus as $ateOyatsu)
        <tr>
            <td>{{ $ateOyatsu->oyatsu->name }}</td>
            <td>{{ $ateOyatsu->oyatsu->calory }}</td>
            <td>{{ $ateOyatsu->ate_at }}</td>
        </tr>
    @endforeach
</table>
<h2>総カロリー</h2>
<p>{{ $totalCalory }}</p>
</body>
```

で、普通に同期通信してるので、「送信」ボタンを押すと画面がチラッてするんですよね。
ダメじゃないんですけど、古めかしいと言うか、イマイチな感じがして「んー」ってなりますよね。

# Inertiaじゃない場合（ajax使う場合）

そうなるとやっぱ非同期通信ですよね。
非同期通信といえば俺達のjQueryの出番です。。。って言おうと思ったんですがめんどくさいのでAlpineJS&axiosにしました。
しかしAlpineJS使ったとて思ったより大変になってしまいました。
（だいぶ忘れてたのもありますが）
おやつだからではないですが、だいぶハイカロリーでした。

```php:example.ateOyatsuAjax.blade.php
@routes

<head>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div x-data="Oyatsu()">
    <h1>おやつを食べる</h1>
    <form>
        <select name="oyatsu_id" x-model="selectedOyatsu">
            <option value="">選択してください</option>
            <template x-for="oyatsu in oyatsus">
                <option :value="oyatsu.id" x-text="`${oyatsu.name} (${oyatsu.calory}kcal)`"></option>
            </template>
        </select>
        <input type="date" name="ate_at" x-model="ateAt">
        <button type="button" @click="submitOyatsu">送信</button>
    </form>
    <hr>
    <h2>食べたおやつ</h2>
    <table>
        <tr>
            <th>名前</th>
            <th>カロリー</th>
            <th>食べた日</th>
        </tr>
        <template x-for="ateOyatsu in ateOyatsus">
            <tr>
                <td x-text="ateOyatsu.oyatsu.name"></td>
                <td x-text="ateOyatsu.oyatsu.calory"></td>
                <td x-text="ateOyatsu.ate_at"></td>
            </tr>
        </template>
    </table>
    <h2>総カロリー</h2>
    <p x-text="totalCalory"></p>
</div>
<script defer>
    function Oyatsu() {
        return {
            oyatsus: @json($oyatsus),
            ateOyatsus: @json($ateOyatsus),
            selectedOyatsu: null,
            ateAt: null,
            totalCalory() {
                return this.ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);
            },
            submitOyatsu() {
                axios.post(route('example.ateOyatsu.bladeAjax.update'), {
                    oyatsu_id: this.selectedOyatsu,
                    ate_at: this.ateAt
                }).then(response => {
                    if (response.data.status === 'success') {
                        this.ateOyatsus.push(response.data.ateOyatsu);
                    }
                });
            }
        }
    }
</script>
</body>
```

で、注目はここですよね。

```js
                axios.post(route('example.ateOyatsu.bladeAjax.update'), {
                    oyatsu_id: this.selectedOyatsu,
                    ate_at: this.ateAt
                }).then(response => {
                    if (response.data.status === 'success') {
                        this.ateOyatsus.push(response.data.ateOyatsu);
                    }
                });
```

ここがまさにAPIを呼んでいるところであり、めんどくさいポイントになります。
パッと見、コード量的には大した事なさそうに見えますが、リクエストの形どうするのか、成功したら何返すのか、バリデーションエラー出たらどうするのかと考えることはとても多いのです。

# Inertia(React)の場合

ざっとこんな感じになります。
ちゃんとスタイリングしないと見た目が変わっちゃうのですが、機能的には同じなので良しとしましょう。

```tsx:Pages/Example/ateOyatsu.tsx
import {useForm} from "@inertiajs/react";

type Oyatsu = {
  id: number;
  name: string;
  calory: number;
};

type AteOyatsu = {
  id: number;
  oyatsu_id: number;
  ate_at: string;
  oyatsu: Oyatsu;
};

type Props = {
  oyatsus: Oyatsu[];
  ateOyatsus: AteOyatsu[];
}

export default function AteOyatsu({oyatsus, ateOyatsus}: Props) {
  const {data, setData, post, progress, processing} = useForm({
    oyatsu_id: '',
    ate_at: '',
  });

  const totalCalory = ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);

  const submitOyatsu = () => {
    if (!processing) {
      post(route('example.ateOyatsu.inertia.update'));
    }
  };

  return (
      <div>
        <h1>おやつを食べる</h1>
        <form>
          <select name="oyatsu_id" onChange={(e) => setData('oyatsu_id', e.target.value)}>
            <option value="">選択してください</option>
            {oyatsus.map((oyatsu, index) => (
                <option value={oyatsu.id} key={`option${index}`}>{oyatsu.name} ({oyatsu.calory}kcal)</option>
            ))}
          </select>
          <input type="date" name="ate_at" onChange={(e) => setData('ate_at', e.target.value)}/>
          <button type="button" onClick={submitOyatsu}>送信</button>
        </form>
        <hr/>
        <h2>食べたおやつ</h2>
        <table>
          <thead>
            <tr>
              <th>名前</th>
              <th>カロリー</th>
              <th>食べた日</th>
            </tr>
          </thead>
          <tbody>
            {ateOyatsus.map((ateOyatsu, index) => (
                <tr key={`oyatsuRaw${index}`}>
                  <td>{ateOyatsu.oyatsu.name}</td>
                  <td>{ateOyatsu.oyatsu.calory}</td>
                  <td>{ateOyatsu.ate_at}</td>
                </tr>
            ))}
          </tbody>
        </table>
        <h2>総カロリー</h2>
        <p>{totalCalory}</p>
      </div>
  )
}
```

ポイントとしてはここですね。

```tsx
  const {data, setData, post, processing} = useForm({
    oyatsu_id: '',
    ate_at: '',
  });

// 略

  const submitOyatsu = () => {
    if (!processing) {
      post(route('example.ateOyatsu.inertia.update'));
    }
  };
```

普通のReactでやるときはフォーム側で設定した値を `useState` とかで状態管理する必要があるのですが、Inertia用Reactでは `useForm` という専用のフックがあって、それを使って諸々の事ができるようになっています。

`useForm` すると以下の変数と関数が取れます。
他にも色々取れるのですけど、今回使う分だけ紹介します。

|変数/関数名|役割|
|---|---|
|data|postで送信する時のデータを格納しておく箱|
|setData|dataに値をセットするための関数|
|post|dataに入っているデータをpostでサーバーに送る関数|
|processing|通信中かどうかのboolean値が入ってる変数。連打対策とかで使う|

なので、最初に `useForm` で必要な変数関数を貰って、
`setData` でデータをセットして、 `post` するだけでサーバーにデータを送ることができます。

Ajaxでは `Axios.post` で送信したところの続きに `then` でコールバック処理を書いてましたが、そんな質めんどくさいことも不要です。
受け側のControllerの戻りを `redirect()` にして、戻り先をもとのページにするだけで画面遷移することなく画面をリフレッシュできます。

なので、APIとかいちいち用意しなくても非同期通信ができて、いい感じにできるという話でした！