Vue.js + Laravel 開発で知っておきたいフォーム送信のベストプラクティス
はじめに
Vue.jsとLaravelでWebアプリを開発していると、必ずぶつかるのがフォーム送信の処理です。
「ユーザー情報を送信したい」「バリデーションエラーを返したい」「送信中はボタンを無効化したい」など、細かいUXを考えると、意外と実装量が多くなります。
僕自身も最初は普通にaxiosなどを使ってAPIを叩いていましたが、Inertia.jsのuseFormを使ってみたら圧倒的に開発が楽になることに感動しました!
この記事では、通常のAPI通信とuseFormを比較しながら、僕が感じたメリット・デメリットを紹介します。
通常のAPI通信とInertia.jsのuseFormを比較してみた
通常のAPI通信でフォームを送信する場合
もっともオーソドックスな方法はaxiosやfetchでLaravelのAPIにリクエストを投げる方法です。
<template>
<form @submit.prevent="submit">
<div>
<label>メール</label>
<input v-model="email" type="email" />
<!-- バリデーションエラーを表示 -->
<div v-if="errors.email">{{ errors.email[0] }}</div>
</div>
<div>
<label>パスワード</label>
<input id="password" v-model="password" type="password" />
<!-- バリデーションエラーを表示 -->
<div v-if="errors.password">{{ errors.password[0] }}</div>
</div>
<button type="submit" :disabled="processing">Login</button>
</form>
</template>
<script setup>
import { ref } from "vue";
import axios from "axios";
// フォームの値
const email = ref("");
const password = ref("");
// バリデーションエラーを保持する変数
const errors = ref({});
// 送信中フラグ
const processing = ref(false);
const submit = async () => {
processing.value = true; // 送信開始
try {
// フォームをサーバーに送信
await axios.post("/login", {email: email.value, password: password.value });
// 成功したらエラーをリセット
errors.value = {};
} catch (e) {
// バリデーションエラーが返ってきた場合
if (e.response?.status === 422) {
errors.value = e.response.data.errors; // エラー内容をVue側で保持
}
} finally {
processing.value = false; // 送信終了
}
};
</script>
ざっくり解説
- ユーザーが入力した
emailとpasswordをv-modelでVueにバインド - フォーム送信時に
submit関数が呼ばれ、AxiosでサーバーにPOST - 送信中は
processingにtrueを代入してボタンを無効化(二重送信防止) - バリデーションエラーが返ると
errorsに格納してテンプレートで表示
メリット
・APIが独立しているので、モバイルアプリや外部サービスでも使い回せる
・VueとAxiosの知識だけで実装できる
デメリット
・エラーハンドリングやローディング管理を毎回書く必要がある
・Laravelのflashメッセージを受け取る場合、自前でstateに取り込む必要がある
・同じようなフォームが増えるとコードが冗長になりやすい
個人的には、複雑なWebアプリになるほど「同じ処理の繰り返し」がストレスになりがちかなと思ってます。
Inertia.jsのuseFormでフォームを送信する場合
Inertia.jsにはuseFormという便利なフックがあります。
実際に使ってみると、「今まで書いていたバリデーションやローディング管理のコード、ほとんどいらないのでは!?」と感動しました。
<template>
<form @submit.prevent="submit">
<div>
<label>メール</label>
<input type="email" v-model="form.email">
<!-- バリデーションエラーを表示 -->
<div v-if="form.errors.email">{{ form.errors.email }}</div>
</div>
<div>
<label>パスワード</label>
<input type="password" v-model="form.password">
<!-- バリデーションエラーを表示 -->
<div v-if="form.errors.password">{{ form.errors.password }}</div>
</div>
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
<script setup>
import { useForm } from "@inertiajs/vue3";
const form = useForm({
email: "",
password: "",
});
const submit = () => {
form.post("/login");
};
</script>
ざっくり解説
-
useFormでフォームの値や状態(errors、processingなど)をまとめて管理 -
v-modelでform.emailやform.passwordに入力値をバインド - フォーム送信時に
submit関数が呼ばれ、form.postで自動的にPOSTリクエストを送信 -
form.processingが自動的にtrueになり、ボタンを無効化(二重送信防止) - バリデーションエラーは自動で
form.errorsに格納され、テンプレートで表示
メリット
・フォームの値の管理が楽
・エラーメッセージが自動で管理される
・送信中の状態管理も簡単
・ページ遷移がSPA的にシームレス
デメリット
・モバイルアプリや他サービスとAPIを共用したい場合には不向き。
・Laravelなどのバックエンドと組み合わせることを前提に設計されているため、バックエンドの構造や仕様に依存しやすい
メリットを挙げただけではイメージしづらいと思うので、この中から実際のコード例を交えつつ、具体的にどう便利なのかを簡単に解説していきます。
1. エラーメッセージが自動で管理される
<div v-if="form.errors.password">{{ form.errors.password }}</div>
Laravel側でバリデーションエラーが発生すると、Inertiaのform.errorsに自動で格納されます。
つまり、Vue側では特別な処理を書かなくても、フォームにエラーメッセージを簡単に表示できます。
例えば、パスワードに関するバリデーションをLaravel側で以下のように設定したとします。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function rules()
{
return [
'password' => 'required|min:8',
// 略
];
}
public function messages()
{
return [
'password.min' => 'パスワードは8文字以上で入力してください。',
];
}
}
この状態で、ユーザーが8文字未満のパスワードを入力すると、サーバーから以下のようなレスポンスが返ります。
{
"errors": {
"password": [
"パスワードは8文字以上で入力してください。"
]
}
}
返されたバリデーションエラーの配列を自動的に単一の文字列のメッセージに変換してform.errorsに格納します。
そのため、通常のAPI通信の例(errors.password[0])とは異なり、直接form.errors.passwordでメッセージを表示することができます。
2. 送信中の状態管理も簡単
<button type="submit" :disabled="form.processing">Login</button>
上記のようにdisabledにform.processingをバインドすることで、送信中のローディング状態を簡単に制御できます。
- フォーム送信が始まると自動で
form.processingがtrueになり、ボタンが無効化されます - リクエストが完了(成功または失敗)すると自動で
falseに戻り、再度ボタンをクリックできるようになります
これにより、ユーザーによる二重送信を防ぎつつ、送信中の状態をUIに反映させることが簡単にできます。
3. その他便利な機能
useFormには、エラーメッセージや送信状態以外にも便利なメソッドやフックが用意されています。いくつか代表的なものを紹介します。
-
onSuccess:送信処理が成功した場合に実行される処理
サーバーから成功レスポンス(HTTPステータス2xxまたはリダイレクト)が返ってきた場合に実行されるコールバックです。成功メッセージの表示や、モーダルを閉じるなどの後処理に適しています。
<script setup>
//~ 略 ~
const submit = () => {
form.post("/login", {
onSuccess: () => console.log("成功"),
});
};
</script>
-
onFinish:送信処理が完了した場合に実行される処理(成功/失敗どちらでも)
リクエストが成功したか失敗したかに関わらず、必ず最後に実行されるコールバックです。form.processingをfalseに戻す直前に呼ばれます。
<script setup>
//~ 略 ~
const submit = () => {
form.post("/login", {
onFinish: () => {
console.log("処理が完了しました");
},
});
};
</script>
-
form.reset:フォームの状態をリセットする
フォームの入力内容やエラー、変更があったかの状態を初期値に戻します。引数なしで呼ぶと全てのフィールドが、引数にキー名を指定すると特定のフィールドだけがリセットされます。
<script setup>
//~ 略 ~
const submit = () => {
form.post('/login', {
onSuccess: () => form.reset(),
})
}
</script>
-
transform:送信前にデータを加工する
フォームデータをサーバーに送信する直前に、その内容を変更・加工するためのメソッドです。
<script setup>
//~ 略 ~
const submit = () => {
form.transform((data) => ({
...data,
email: data.email.toLowerCase(), // メールを小文字に変換して送信
}));
form.post('/login');
};
</script>
API通信とuseFormどっちを選ぶ?
個人的な意見ですが、僕はWebアプリをInertia.js前提で作るならuseForm一択だと思います。
正直、通常のAPI通信でやるメリットは「モバイルや外部クライアントと共用できること」くらいで、Webアプリだけならほとんどデメリットしか感じません。
| 項目 | API通信 | Inertia.js + useForm |
|---|---|---|
| 実装のシンプルさ | ✗ エラーハンドリングやローディング管理を毎回書く | ◎ 自動管理されるので短いコードで済む |
| バリデーションエラー | ✗ 自分でstateに格納する必要あり |
◎ 自動で form.errors に格納 |
| ローディング管理 | ✗ 自分でフラグを用意 | ◎ form.processing が便利 |
| flashメッセージ | ✗ 自前でstateに反映 |
◎ Laravelのflashメッセージをそのまま利用可能 |
| 外部 API 共有 | ◎ しやすい | △ Inertia.js前提なので非Webクライアントは使いにくい |
まとめ
上記でも述べた通り、WebアプリをInertia.js前提で作る場合、フォーム管理にはuseFormが非常に便利です。
特にフォームが多いアプリや、バリデーション・ローディング・フラッシュメッセージの管理を自動化したい場合に最適です。
ただし、外部APIやモバイルクライアントと共用する場合は、通常のAPI通信の方が適しているケースもあるので、プロジェクトの状況に応じて判断する必要があります。
個人的には、コード量が減るだけでなく、状態管理やUXの不一致の心配も少なくなるため、チーム開発の現場でも非常に助かると思います。
最後までお読みいただき、ありがとうございました🙇♂️
Discussion