Inertia.jsでXHRリクエスト Inertia.js入門 #6

Laravel

この記事では、Inertia.jsで非同期通信を行うrouter.visit()について解説します。

前回の記事ではInertia LinkでXHRリクエストを行う方法について解説しました。前回の記事はこちらからご覧ください。

Laraveler

Inertia.jsで非同期通信はどうやったらいいんでしょうか?

シップ

Inertia.jsで非同期通信を行うためのメソッドがrouter.visit()です。フォームの送信にも使えますよ!

XHRリクエストとは

前回Inertia LinkでリンクやボタンからXHRリクエストを作成する方法を学びました。そもそもXHRリクエストとは、XMLHttpRequestの略で、JavascriptからサーバーへHTTPリクエストを行うことです。

SPA(シングルページアプリケーション)では、初回のアクセス時にすべてのHTML構造を読み込み、その後は必要なデータだけを必要に応じて読み込んで画面を更新していきます。その必要なデータを読み込む際に必要なのが、XHRリクエストです。

Inertia.jsでは、初回以降サーバーへのリクエストをInertiaリクエストによって行います。そのInertiaリクエストを作成するのが、前回説明したInertia Linkや、今回説明するrouter.visit()です。

router.visit()を使うことで、フォームの送信やファイルアップロードが行えます。

router.visit()の使い方

メソッドの呼び出し方と引数

router.visit()は以下の引数を取ります。

import { router } from '@inertiajs/vue3'

router.visit(url, {
  method: 'get',
  data: {},
  replace: false,
  preserveState: false,
  preserveScroll: false,
  only: [],
  headers: {},
  errorBag: null,
  forceFormData: false,
  onCancelToken: cancelToken => {},
  onCancel: () => {},
  onBefore: visit => {},
  onStart: visit => {},
  onProgress: progress => {},
  onSuccess: page => {},
  onError: errors => {},
  onFinish: visit => {},
})

各HTTPメソッド専用の関数

router.visit()では引数のmethodでどのHTTPメソッドを使用するか指定できますが、GET用、POST用など専用の関数も用意されています。第一引数でURL、第二引数にデータ配列、そしてvisit()と同じオプションを第三引数で指定できます。

import { router } from '@inertiajs/vue3'

router.get(url, data, options)
router.post(url, data, options)
router.put(url, data, options)
router.patch(url, data, options)
router.delete(url, options)
router.reload(options) // 現在のURLを再読み込みする。preserveState と preserveScroll はtrueにセットされる。

router.visit()や各種専用メソッドのオプション

HTTPメソッド

POSTリクエストを作成するには、2つの方法があります。専用のメソッドを使うか、visitメソッドのmehodオプションにpostを指定するかです。

import { router } from '@inertiajs/vue3'

//Inertia.postメソッドを利用する方法
router.post(url, data, options)

//Inertia.visitのmethodオプションに'post'を指定する方法
router.visit(url, { method: 'post' })

putまたはpatchメソッドでのファイルアップロードはLaravelではサポートされていないため実際にはpostメソッドを使用してリクエストが行われます。

送信データ

送信するデータはget(),put(),post(),patch()メソッドを使用する場合は第二引数で,visit()メソッドを使用する場合はdataオプションで指定します。

import { router } from '@inertiajs/vue3'

//専用メソッドを使用する場合
router.post('/Bookmark/store',{
    title: 'Yahoo!', 
    url: 'https://www.yahoo.co.jp/',                    
});

//visitメソッドを使用する場合
router.visit('/Bookmark/store',{
    method: 'post',
    data:{
        title: 'Yahoo!', 
        url: 'https://www.yahoo.co.jp/',   
    }
});

履歴管理

Inertia.jsは、異なるURLへのリクエストが行われたとき、自動的にそのURLをブラウザ履歴に追加しますが、replaceオプションをtrueにセットすると、ページ移動の際に履歴を追加するのではなく、現在の履歴を書き換えるようにすることができます。

import { router } from '@inertiajs/vue3'

router.visit('/Bookmark/search/google',{ replace: true });

状態の保持

デフォルトではInertiaリクエストによって新たなページが読み込まれると、フォームに入力された値やスクロール位置などはリセットされます。preserveStateオプションをtrueにセットすると、それらの状態を維持してリクエストを行うことができます。

例えばフォーム送信する際に、バリデーションの結果エラーがあった場合などフォームの内容をクリアせずにおきたいときなどに使用します。

post, put, patch, delete, reloadの各メソッドではデフォルトでpreserveStateオプションはtrueにセットされています。

import { router } from '@inertiajs/vue3'

router.visit('/Bookmark/search/google',{ preserveState: true });

レスポンスに応じてpreserveStateを後からセットすることもできます。エラーがある場合はフォームの内容を残しておき、エラーがなければクリアするなどの用途に使えます。

//検索結果が見つからない場合はフォーム入力値をそのまま残す
import { router } from '@inertiajs/vue3'

router.visit(route('bookmark.search', this.searchWord),{ 
    preserveState: function(page){ 
			return Object.keys(page.props.bookmarks).length == 0 
		   }, 
});

スクロールポジションの保持

ページ間の移動をする場合、デフォルトの動作はスクロールポジションがリセットされ、ページ最上部へと移動します。これをpreserveScrollオプションをtrueにセットすることで防げます。

import { router } from '@inertiajs/vue3'

router.visit(route('bookmark.search', 'google'),{ preserveScroll: true });

部分的なデータ読み込み

そのページに含まれるデータのうち、ある特定のデータだけを読み込めばいい場合、onlyオプションでその読み込むデータを指定できます。

import { router } from '@inertiajs/vue3'
//bookmarksだけを読み込みたい場合
router.visit(route('bookmark.search', 'google'),{ only: ['bookmarks'] });

エラーバッグ名

同じページに複数のフォームがあり、さらに同じ名前の項目がある場合、どのフォームでバリデーションエラーが発生したのかを識別するための名前をerrorBagオプションで指定します。

(関係する部分のみ抽出しています。フルのコードはソースコードからご覧ください)

//Vue側のエラー表示
<div v-if="bookmarkError.url" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.url }}</div>

//Script setup
//computedをインポート
import { computed } from "vue";

//propsを定義
const props = defineProps({ 
    errors: {
        type: Object
    }
});

//post
const addBookmark = () => {
    router.visit(route('bookmark.store'),{
        method: 'post',  //POSTメソッドで送信
        data:{
            title: newTitle.value, //送信データを指定
            url: newUrl.value,
        },
        errorBag: 'addBookmark',    //エラーバッグ名を指定
    });
};

//computedでエラーバッグ内のエラーを計算
const bookmarkError = computed(() => {
    return props.errors.addBookmark || {}
});

//コントローラー側でのバリデーション
$validator = Validator::make($request->all(), [
	'title' => ['required', 'string', 'max:255'],
	'url' => ['required', 'string', 'max:255'],
])->validate();

//エラーがあった場合のpropsのJSONデータ(一部)
"errors":{
	"addBookmark":{
		"title":"The title field is required.",
		"url":"The url field is required."}
	}

ファイルアップロード

Inertiaリクエストにファイルが含まれる場合、自動的にFormDataオブジェクトに変換されて送信されます。forceFormDataオプションをtrueにセットすると、強制的にFormDataオブジェクトに変換して送信が行われます。

this.$inertia.post('/companies', data, {
  forceFormData: true,
})

カスタムヘッダー

HTTPヘッダーに独自のヘッダーを追加してリクエストしたい場合はheadersオプションで指定します。

import { router } from '@inertiajs/vue3'

router.post('/users', data, {
  headers: {
    'Custom-Header': 'value',
  },
})

リクエストのキャンセル

Inertiaリクエストを行った後で、中止したい場合は発行されたキャンセルトークンのcancel()メソッドを呼び出すことでリクエストをキャンセルできます。キャンセルされた場合onCancel()onFinish()イベントコールバックが呼び出されます。

キャンセルトークンは、onCancelToken()コールバックで取得できます。

通常のリクエストではキャンセルする間もなくリクエストが完了するので、時間のかかる処理やファイルアップロードなどをキャンセルする時に使えます。

//キャンセルボタンを表示
<button v-if="cancelToken.cancel" v-on:click="cancelToken.cancel()" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>

<script setup>
import { reactive} from "vue";

//VueのDataとしてcancelTokenを定義
const cancelToken = reactive({cancel: false});

//router.visitを行うメソッド
router.visit(route('bookmark.store'),{
        method: 'post',  //POSTメソッドで送信
        data:{
            title: newTitle.value, //送信データを指定
            url: newUrl.value,
        },
        onCancelToken: (_cancelToken) => (Object.assign(cancelToken, _cancelToken)),   //キャンセルトークンが発行されたらキャンセルトークンを保存
        onCancel: () => {console.log( 'onCancel' ) },   //リクエストキャンセル時
        onFinish: visit => {Object.assign(cancelToken, {cancel: false}); console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される

    });

イベントコールバック

Inertiaリクエストの進行状況に合わせてイベントコールバックが呼び出されます。

onBefore : リクエスト直前

onBefore()コールバックはリクエストを行う直前に呼び出されます。falseを返すとリクエストがキャンセルされます。例えば、confirmダイアログを表示して確認を行えます。

import { router } from '@inertiajs/vue3'

router.delete(`/users/${user.id}`, {
  onBefore: () => confirm('本当に削除しますか?'),
})

onStart : リクエスト開始時

Inertiaリクエストが開始されたときにonStart()が呼び出されます。コールバックの引数でrouter.visitに渡したURLやデータ、オプションがオブジェクトで取得できます。

onProgress : リクエスト進行中

Inertiaリクエストの進行中にonProgress()が呼び出されます。ファイルアップロードの状況確認などに使うことができます。引数で進行具合のオブジェクトを取得できます。

onSuccess : リクエスト成功時

Inertiaリクエストが成功し、結果が返ってきたときにonSuccess()が呼び出されます。引数では結果として返ってきた新しいページデータが取得できます。

onError : エラー時

Inertiaリクエストが失敗したり、エラーが返ってきた場合にonError()が呼び出されます。引数ではエラーオブジェクトが渡されます。

//渡されるエラーオブジェクトの例。
//errorBagでエラーバッグ名を渡していてもエラーオブジェクトには含まれないことが分かります。
 { title: "The title field is required.", url: "The url field is required." }

onCancel : キャンセル時

前項で説明したキャンセルトークンを利用してリクエストのキャンセルが行われたときにonCancel()が呼び出されます。

onFinish : リクエスト完了時

リクエストが成功しようが失敗しようが、キャンセルされようがonFinish()は最後に呼び出されます。onSuccess()onError()コールバックの中でPromiseでメソッドを返すと、それらが処理された後でonFinish()は呼び出されます。

import { router } from '@inertiajs/vue3'

router.post(url, {
  onSuccess: () => {
    return Promise.all([
      this.doThing(),
      this.doAnotherThing()
    ])
  }
  onFinish: visit => {
    // ここは doThing() と doAnotherThing() の処理が終わってから呼び出されます。
  },
})

実例

//各イベントコールバックの状態をlogに出力します。
import { router } from '@inertiajs/vue3'

router.visit(route('bookmark.store'),{
    method: 'post',
    data:{
        title: this.newTitle, 
        url: this.newUrl,
    },
    onBefore: (visit) => confirm('追加しますか?'),
    onStart: (visit) => { console.log( visit ) },
    onProgress: (progress) => { console.log( progress ) },
    onSuccess: (page) => { console.log(  page ) },
    onError: (errors) => {console.log( errors ) },
    onCancel: () => {console.log( 'onCancel' ) },
    onFinish: visit => {console.log( 'onFinish' )},
});

サンプル

今回使用したrouter.visit()を使ったサンプルです。すべてのソースコードはGitHubにも掲載しています。

<script setup>
import { ref, reactive, computed } from "vue";
import { Link, router } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'

const props = defineProps({ 
    bookmarks: {
        type: Array
    }, 
    errors: {
        type: Object
    }
});
const newTitle = ref('');
const newUrl = ref('');
const cancelToken = reactive({cancel: false});

const bookmarkError = computed(() => {
    return props.errors.addBookmark || {}
});

const addBookmark = () => {
    router.visit(route('bookmark.store'),{
        method: 'post',  //POSTメソッドで送信
        data:{
            title: newTitle.value, //送信データを指定
            url: newUrl.value,
        },
        errorBag: 'addBookmark',    //エラーバッグ名を指定
        preserveState: function(page){  //フォームの内容を保持するかどうか
            return Object.keys(page.props.errors).length != 0 //エラーがあればフォームの内容を保持する
        },
        preserveScroll: true,   //スクロールポジションを維持する
        onCancelToken: (_cancelToken) => (Object.assign(cancelToken, _cancelToken)),   //キャンセルトークンが発行されたらキャンセルトークンを保存
        onBefore: (visit) => confirm('追加しますか?'), //リクエスト送信前に確認ダイアログを表示し、その結果に応じて続行・キャンセル
        onStart: (visit) => { console.log( visit ) },   //リクエスト開始時に
        onProgress: (progress) => { console.log( progress ) },  //リクエスト進行中
        onSuccess: (page) => { console.log(  page ) },  //リクエスト成功時
        onError: (errors) => {console.log( errors ) },  //エラー発生時
        onCancel: () => {console.log( 'onCancel' ) },   //リクエストキャンセル時
        onFinish: visit => {Object.assign(cancelToken, {cancel: false}); console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される

    });
};
</script>

<template>
    <Layout title="ブックマーク一覧">
        <h2 class="text-lg border-b p-2">
            <Link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </Link>
        </h2>
        <ul class="list-disc list-inside p-2">
            <li v-for="bookmark in bookmarks" class="list-none flex justify-between">
                <a :href="bookmark.url" target=_blank class="text-blue-700" >{{ bookmark.title }}</a>
                <Link as="button" method="delete" :href="route('bookmark.delete',bookmark.id)" preserve-scroll :only="['bookmarks']" class="border border-red-400 m-1 p-1 text-sm text-red-400">削除</Link>
            </li>
        </ul>
        <h3 class="border-l-4 p-1">ブックマーク追加</h3>
        <div class="flex w-full">
            <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">タイトル</div>
            <div class="w-40">
                <input type=text name=newTitle v-model="newTitle" size=15 class="p-1 m-1 text-sm w-full border" />
            </div>
            <div v-if="bookmarkError.title" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.title }}</div>

        </div>
        <div class="flex w-full">
            <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">URL</div>
            <div class="w-40">
                <input type=text name=newUrl v-model="newUrl" size=15 class="p-1 m-1 text-sm w-full border" />
            </div>
            <div v-if="bookmarkError.url" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.url }}</div>
        </div>
        <button v-on:click="addBookmark" class="border border-gray-400 m-1 p-1 text-sm">ブックマーク追加</button>
        <button v-if="cancelToken.cancel" v-on:click="cancelToken.cancel()" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>
    </Layout>
</template>
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\Bookmark;
use Illuminate\Support\Facades\Validator;

class BookmarkController extends Controller
{
    public function index () 
    {
        return Inertia::render('Bookmark/Index',['bookmarks' => Bookmark::all()]);
    }
    
    //検索メソッド
    public function search ($queryWord) 
    {
        return Inertia::render('Bookmark/Index',[
            'bookmarks' => Bookmark::Where(
                                'title', 'like', '%'.$queryWord.'%'
                            )->orWhere(
                                'url', 'like', '%'.$queryWord.'%'
                            )->get()
        ]);
    }

    //追加メソッド
    public function store (Request $request) 
    {
        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'url' => ['required', 'url', 'max:255', 'unique:App\Models\Bookmark,url'],
        ])->validate();

        $bookmark = new Bookmark;
        $bookmark->title = $request->title;
        $bookmark->url = $request->url;
        $bookmark->save();

        return redirect()->route('bookmark.index', $parameters = [], $status = 303, $headers = []);
    }

    //削除メソッド
    public function destroy ($id) 
    {
        Bookmark::destroy($id);
        return redirect()->route('bookmark.index', $parameters = [], $status = 303, $headers = []);
    }
}
表示例
今回のまとめ
  • router.visit()を使ってXHRリクエストを行える
  • 各種オプションを指定することで動作やイベントコールバックを利用できる

次回はInertia.jsにおけるフォームとリダイレクトを取り上げます。

Inertia.js入門記事一覧
Inertia.jsでシンプルにSPAを構築する Inertia入門#1
Inertia.jsを使うとLaravelやRubyonRailsなどのフレームワーク上でAjax用のAPIやコントローラーを作成しなくても、通常のビューを使う要領でSPA(シングルページアプリケーション)が構築できます。
Laravel 10+Inertia.js+Tailwindインストール Inertia入門#2
この記事ではLaravelにInertia.jsをインストールしてHelloWorldを出力する方法までを解説します。
Inertia.jsのルーティングとレスポンス作成 Inertia入門#3
Laravel8でjetstreamを入れるとLimewireかInertiaかを選べます。この記事ではInertiaを使ってルーティングやInertiaレスポンスの返し方を解説します。
Inertia.jsでページレイアウト Inertia入門#4
この記事では、ページレイアウトについて解説します。親レイアウトに子ページを組み込むことで、ヘッダーやフッターを共通化できます。
Inertia Linkの使い方 Inertia.js入門 #5
この記事では、Inertia.jsのLinkコンポーネントについて解説します。
Inertia.jsでXHRリクエスト Inertia.js入門 #6
この記事では、Inertia.jsで非同期通信を行うrouter.visit()について解説します。
Inertia.jsでのフォームとリダイレクト Inertia.js入門 #7
この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。
Inertiaでのバリデーション Inertia.js入門 #8
この記事では、Inertia.jsを使った際のバリデーションエラーの表示について解説します。
Inertiaで共通データとフラッシュメッセージ Inertia.js入門 #9
この記事では、Inertiaのレスポンスに毎回同じデータを含める方法やフラッシュメッセージの使い方について解説します。
この記事を書いた人

PHPが好物な個人開発プログラマ。フリーランスエンジニアとしてWebサービス作ったりしてます。15年の経験を生かしてMENTAでメンターもやってます。WordPressやPHPでお困りのことがあればご相談に乗りますのでDMください。

Follow on SNS
Laravel
SOHO MIND

Comments