1
/
5

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

こちらは スタジオ・アルカナ Advent Calendar 2024 の12日めの記事になります!

はじめに

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


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

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

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

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

Inertiaじゃない場合(普通のblade)

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

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>
<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使ったとて思ったより大変になってしまいました。
(だいぶ忘れてたのもありますが)
おやつだからではないですが、だいぶハイカロリーでした。

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>
@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>

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

                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);
                    }
                });
                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)の場合

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

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>
  )
}
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>
  )
}

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

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

// 略

  const submitOyatsu = () => {
    if (!processing) {
      post(route('example.ateOyatsu.inertia.update'));
    }
  };
  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 すると以下の変数と関数が取れます。
他にも色々取れるのですけど、今回使う分だけ紹介します。

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

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

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

株式会社スタジオ・アルカナからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社スタジオ・アルカナでは一緒に働く仲間を募集しています
5 いいね!
5 いいね!

同じタグの記事

今週のランキング

今山 豪貴さんにいいねを伝えよう
今山 豪貴さんや会社があなたに興味を持つかも