0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel Inertia】逆に Vue から Laravel へ POST 通信はどうやって送るのよ(Rest-Form編)

Last updated at Posted at 2025-12-07

つづき。

VILT スタックAPI を使わないで、システムを構築するものです。
ではどうやって通信を行うのでしょうか。

フォームの作成

よくある場面としては、データの更新でしょうか。
フォームに入力して POST 通信を送る場面を考えてみます。

以下の記事を参考に、今回利用する DTO クラスの作成を行ってください。
PostData PostTagData

そして PostData を更新する処理を書きます。
PostData には、ユーザー選択の機能があるので users も渡してあげます。

実際の環境では all() は使わないで limit()paginate() で制御してくださいね。
初心者がやりがちなんですが、時限爆弾を埋め込むことになります。。。 :fearful:

routes/web.php
Route::get('/test', [TestController::class, 'index'])->name('tests.index');
Route::post('/test', [TestController::class, 'store'])->name('tests.store');
app/Http/Controllers/TestController.php
namespace App\Http\Controllers;

use App\Data\PostData;
use App\Models\User;
use Inertia\Inertia;

class TestController extends Controller
{
    public function index()
    {
        return Inertia::render('Test', [
            'posts' => fn () => Post::all(),
            'users' => fn () => User::all(),
        ]);
    }

    public function store(PostData $data)
    {
        /** @var Post */
        $post = Post::create($data->except('tags')->all());

        foreach ($data->tags as $tag) {
            $post->tags()->create($tag);
        }

        return redirect()->back();
    }
}

Inertia の処理は redirect() を返却するのが基本です。

back() をすることで先ほどの画面に戻って、自動的に defineProps に書かれているデータのリロード行われます。なので、フロントに再取得処理などは書かなくて大丈夫です。

フロント側の画面はこんな感じで作ってみます。
PostData を更新するためのフォームと、下部に記事一覧を表示しています。

UI には PrimeVue と呼ばれるものを使っていますが、使い慣れたものでOKです。
自作でもOK。

resources/js/pages/Test.vue
<template>
  <div class="m-4">
    <div class="w-md">
      <form class="grid grid-cols-[auto_1fr] items-center gap-2" @submit.prevent>
        <label>ユーザーID</label>
        <Select
          v-model="form.user_id"
          :options="users"
          option-value="id"
          option-label="name"
        />

        <label>タイトル</label>
        <InputText v-model="form.title" />

        <label class="self-start pt-2">コンテンツ</label>
        <Textarea v-model="form.body" rows="3" />

        <label class="self-start pt-2">タグ</label>
        <div class="grid grid-cols-1 gap-2">
          <div
            v-for="(tag, idx) of form.tags"
            :key="idx"
            class="flex items-center gap-2"
          >
            <InputText
              :model-value="tag.name"
              class="grow"
              @update:model-value="form.tags[idx].name = $event ?? ''"
            />
            <ColorPicker
              :model-value="tag.color"
              @update:model-value="form.tags[idx].color = $event"
            />
            <Button label="-" severity="danger" @click="onRemoveTag(idx)" />
          </div>
          <div>
            <Button label="+ 追加" @click="onAppendTag"/>
          </div>
        </div>

        <div class="col-span-2 justify-self-end">
          <Button
            type="submit"
            label="保存"
            @click="onSubmit"
          />
        </div>
      </form>
    </div>

    <div>
      <label>Posts: ({{ posts.length }})</label>
      <pre class="text-xs">{{ posts }}</pre>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useForm, usePage } from '@inertiajs/vue3'

import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
import Textarea from 'primevue/textarea'

import TestRoute from '@/routes/tests'

const form = useForm<App.Data.PostData>({
  user_id: 0,
  title: '',
  body: undefined,
  tags: [{ name: '', color: '' }]
})

defineProps<{
  posts: App.Models.Post[],
  users: App.Models.User[],
}>()

const onAppendTag = () => { form.tags.push({ name: '' }) }
const onRemoveTag = (idx: number) => { form.tags.splice(idx, 1) }

const onSubmit = () => {
  form.submit(TestRoute.store())
}
</script>

当たり前のように出てきますが、モデル型やリクエストの型は自動生成させます。
素晴らしすぎるぅ、、、 :hugging:

image.png

この画面で保存ボタンを押すと...?

image.png

保存されました~。しかも下部に表示されているデータは更新されているという現象になります。
これが Inertia.js の面白い仕組みなんです。

Inertia.jsSPA のように振る舞うため、同じ画面へ遷移する場合は props のデータのみ更新がかかります。
便利ですねぇ。

▼ エラーハンドリングは??

安心してください。
もし検証に失敗すると、自動で form.errors にいつものバリデーションエラーが格納されます。
それを画面に表示すればOK。

{{ form.errors.user_id }}

// 配列の場合はこんな感じで index で指定してあげる
{{ form.errors?.[`tags.${idx}.name`] }}

image.png

▼ 保存に成功したらフォームをリセットしたいんだけど

できます。form.submit() には各種イベントをキャッチできる仕組みが存在します。
その中の onSuccess に、実行したいクロージャーを渡すことで、更新に成功したらフォームの値をリセットすることができます。

const onSubmit = () => {
  form.submit(TestRoute.store(), {
    onSuccess: () => form.reset()
  })
}

ちなみに、値は一番初めに useForm({ ここ }) で指定した値となります。

もし変更したい場合は defaults({ ここ }) に書くことで上書きすることができます。
これはダイアログなどで、選択したレコードを更新するときに便利です。

const selectedPost = {
user_id: 2,
title: '更新対象のポスト',
body: '更新するよ~',
tags: [{ name: 'タグだよ', color: '00FF00' }]
// etc...
}

form.defaults({
user_id: selectedPost.user_id,
title: selectedPost.title,
body: selectedPost.body,
tags: selectedPost.tags,
})

補足として、この仕組みを使えば画面のローディングなども制御できますね。

▼ すべてのデータが自動で変わると通信量が増えない?

今回で言うと postsusers が自動的に再取得されるので、DBへのアクセスが発生します。もし複雑な SQL を組んでいると更新に時間がかかる可能性があります...。
(実は share() で指定したものも併せて更新されちゃいます)

image.png

そんな時は only パラメタを使います!

const onSubmit = () => {
  form.submit(TestRoute.store(), {
    only: ['posts'],
    onSuccess: () => form.reset(),
  })
}

すると返却値がこんなに小さくなります!

image.png

これでポイントになってくるのが、コントローラー側のクロージャーです。

public function index()
{
    return Inertia::render('Test', [
        'posts' => fn () => Post::with('tags')->get(), // ※ここだけ実行される!
        'users' => fn () => User::all(),
    ]);
}

クロージャーの外に書いてあるとどんな通信でも実行してしまうのですが、属性値を fn() で囲っておくと、only を利用した際に、未指定の属性の関数は実行されません!
今までの Laravel とは違った思考が必要かもしれませんね。

なので、基本的には全部クロージャーでくくっておくと最適化ができます。

Inertia.js は複雑に絡み合っているから、一つ一つの説明が難しい。。。

▼ 味気ないから成功時に Toast を送りたいな

前章で書いた share() を利用します。
ここに toast を増やして、セッションに記録されたトーストを持ってくるようにします。この時必ず fn () でくくってください。

app/Http/Middleware/HandleInertiaRequests.php
    public function share(Request $request): array
    {
        [$message, $author] = str(Inspiring::quotes()->random())->explode('-');

        return [
            ...parent::share($request),
            'name' => config('app.name'),
            'quote' => ['message' => trim($message), 'author' => trim($author)],
            'auth' => [
                'user' => $request->user(),
            ],
+           'toast' => fn() => request()->session()->get('toast'),
        ];
    }

これを TypeScript の型にも記載します。

resources/js/types/index.d.ts
export type AppPageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
    name: string;
    quote: { message: string; author: string };
    auth: Auth;

+   toast?: string
};

そしてコントローラーで、リダイレクトをかける際に with() でメッセージを書き込みます。

app/Http/Controllers/TestController.php
    public function store(PostData $data)
    {
        /** @var Post */
        $post = Post::create($data->except('tags')->all());

        foreach ($data->tags as $tag) {
            $post->tags()->create($tag->all());
        }

-       return redirect()->back();
+       return redirect()->back()
+           ->with('toast', '保存しました。');
    }

フロントでは、この toast が変化した際に、トーストを発行します。
仮で windows.alert() で実装してみます。

const page = usePage()
watch(() => page.props.toast, () => {
  window.alert(page.props.toast)
})

すると、ちゃんとアラートが表示されました。
この watch() をレイアウトなどに仕込んでおけば、好きにトーストを発行できます。

image.png

もし、表示されないんやが?という人は only 設定を見てみてください。
実はこの only 、share データにも影響を与えるので、こちらに toast をついかするひつようがあるんですねぇ。奥深い。

  form.submit(TestRoute.store(), {
-   only: ['posts'],
+   only: ['posts', 'toast'],
    onSuccess: () => form.reset(),
  })

終わりに

このあたりが扱えれば Form 作りには何も困らないと思いますね。
もしユーザーのページネートがしたければ、この only を使って、ユーザーデータだけ持ってきたりできるってわけですね。

ただその時は、 Ajax 通信をしたほうが良いかもしれない。。。

ちなみに useForm の値の挙動がわかんない。。というときは以下の記事を参考にしてください。

最近は <Form> タグ上で useForm の仕組みを扱うこともできるようになったので、シンプルなお問い合わせページであれば userForm を使うまでもなく簡単に実装できます!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?