この記事では、Inertia.jsで非同期通信を行うInertia.visit()について解説します。
前回の記事ではInertia linkでXHRリクエストを行う方法について解説しました。前回の記事はこちらからご覧ください。

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

Inertiaで非同期通信を行うためのメソッドがInertia.visit()です。フォームの送信にも使えますよ!
XHRリクエストとは
前回Inertia LinkでリンクやボタンからXHRリクエストを作成する方法を学びました。そもそもXHRリクエストとは、XMLHttpRequestの略で、JavascriptからサーバーへHTTPリクエストを行うことです。
SPA(シングルページアプリケーション)では、初回のアクセス時にすべてのHTML構造を読み込み、その後は必要なデータだけを必要に応じて読み込んで画面を更新していきます。その必要なデータを読み込む際に必要なのが、XHRリクエストです。
Inertia.jsでは、初回以降サーバーへのリクエストをInertiaリクエストによって行います。そのInertiaリクエストを作成するのが、前回説明したInertia Linkや、今回説明するInertia.visit()です。
Inertia.visit()を使うことで、フォームの送信やファイルアップロードが行えます。
Inertia.visit()の使い方
メソッドの呼び出し方と引数
Inertia.visit()は以下の引数を取ります。
this.$inertia.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メソッド専用の関数
Inertia.visitでは引数のmethodでどのHTTPメソッドを使用するか指定できますが、GET用、POST用など専用の関数も用意されています。第一引数でURL、第二引数にデータ配列、そしてVisit()と同じオプションを第三引数で指定できます。
this.$inertia.get(url, data, options)
this.$inertia.post(url, data, options)
this.$inertia.put(url, data, options)
this.$inertia.patch(url, data, options)
this.$inertia.delete(url, options)
this.$inertia.replace(url, options)
this.$inertia.reload(options) // 現在のURLを再読み込みする。preserveState と preserveScroll はtrueにセットされる。
Inertia.visit()や各種専用メソッドのオプション
HTTPメソッド
POSTリクエストを作成するには、2つの方法があります。専用のメソッドを使うか、visitメソッドのmehodオプションにpostを指定するかです。
//Inertia.postメソッドを利用する方法
this.$inertia.post(url, data, options)
//Inertia.visitのmethodオプションに'post'を指定する方法
this.$inertia.visit(url, { method: 'post' })
送信データ
送信するデータはget(),put(),post(),patch()メソッドを使用する場合は第二引数で,visit()メソッドを使用する場合はdataオプションで指定します。
//専用メソッドを使用する場合
this.$inertia.post('/Bookmark/store',{
title: 'Yahoo!',
url: 'https://www.yahoo.co.jp/',
});
//visitメソッドを使用する場合
this.$inertia.visit('/Bookmark/store',{
method: 'post',
data:{
title: 'Yahoo!',
url: 'https://www.yahoo.co.jp/',
}
});
履歴管理
Inertia.jsは、異なるURLへのリクエストが行われたとき、自動的にそのURLをブラウザ履歴に追加しますが、replaceオプションをtrueにセットすると、ページ移動の際に履歴を追加するのではなく、現在の履歴を書き換えるようにすることができます。
this.$inertia.visit('/Bookmark/search/google',{ replace: true });
状態の保持
デフォルトではInertiaリクエストによって新たなページが読み込まれると、フォームに入力された値やスクロール位置などはリセットされます。preserveStateオプションをtrueにセットすると、それらの状態を維持してリクエストを行うことができます。
例えばフォーム送信する際に、バリデーションの結果エラーがあった場合などフォームの内容をクリアせずにおきたいときなどに使用します。
this.$inertia.visit(route('bookmark.search', 'google'),{ preserveState: true });
レスポンスに応じてpreserveStateを後からセットすることもできます。エラーがある場合はフォームの内容を残しておき、エラーがなければクリアするなどの用途に使えます。
//検索結果が見つからない場合はフォーム入力値をそのまま残す
this.$inertia.visit(route('bookmark.search', this.searchWord),{
preserveState: function(page){
return Object.keys(page.props.bookmarks).length == 0
},
});
スクロールポジションの保持
ページ間の移動をする場合、デフォルトの動作はスクロールポジションがリセットされ、ページ最上部へと移動します。これをpreserveScrollオプションをtrueにセットすることで防げます。
this.$inertia.visit(route('bookmark.search', 'google'),{ preserveScroll: true });
部分的なデータ読み込み
そのページに含まれるデータのうち、ある特定のデータだけを読み込めばいい場合、onlyオプションでその読み込むデータを指定できます。
//bookmarksだけを読み込みたい場合
this.$inertia.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>
//Vue側でのpost送信
computed: {
bookmarkError() {
return this.$page.props.errors['addBookmark'] || {}
},
},
methods: {
addBookmark: function(event){
this.$inertia.visit(route('bookmark.store'),{
method: 'post',
data:{
title: this.newTitle,
url: this.newUrl,
},
errorBag: '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オプションで指定します。
this.$inertia.post('/users', data, {
headers: {
'Custom-Header': 'value',
},
})
リクエストのキャンセル
Inertiaリクエストを行った後で、中止したい場合は発行されたキャンセルトークンのcancel()メソッドを呼び出すことでリクエストをキャンセルできます。キャンセルされた場合onCancel()とonFinish()イベントコールバックが呼び出されます。
キャンセルトークンは、onCancelToken()コールバックで取得できます。
//キャンセルボタンを表示
<button v-if="cancelToken" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>
//VueのDataとしてcancelTokenを定義
data:function(){
return {
newTitle: '',
newUrl: '',
cancelToken: null,
}
}
//InertiaVisitを行うメソッド
this.$inertia.visit(route('bookmark.store'),{
method: 'post',
data:{
title: this.newTitle,
url: this.newUrl,
},
onCancelToken: (cancelToken) => (this.cancelToken = cancelToken),
onCancel: () => { console.log("canceled")},
});
イベントコールバック
Inertiaリクエストの進行状況に合わせてイベントコールバックが呼び出されます。
onBefore : リクエスト直前
onBefore()コールバックはリクエストを行う直前に呼び出されます。falseを返すとリクエストがキャンセルされます。例えば、confirmダイアログを表示して確認を行えます。
Inertia.delete(`/users/${user.id}`, {
onBefore: () => confirm('本当に削除しますか?'),
})
onStart : リクエスト開始時
Inertiaリクエストが開始されたときにonStart()が呼び出されます。コールバックの引数でInertia.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()は呼び出されます。
Inertia.post(url, {
onSuccess: () => {
return Promise.all([
this.doThing(),
this.doAnotherThing()
])
}
onFinish: visit => {
// ここは doThing() と doAnotherThing() の処理が終わってから呼び出されます。
},
})
実例
//各イベントコールバックの状態をlogに出力します。
this.$inertia.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' )},
});
サンプル
今回使用したInertia.visit()を使ったサンプルです。すべてのソースコードはGitHubにも掲載しています。
<template>
<layout>
<h2 class="text-lg border-b p-1">
<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" v-bind:class="{'border-red-300': bookmarkError['title']}" />
</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" v-bind:class="{'border-red-300': bookmarkError['url']}" />
</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" v-on:click="cancelToken.cancel()" class="border border-red-300 m-1 p-1 text-sm">キャンセル</button>
</layout>
</template>
<script>
import { Link } from '@inertiajs/inertia-vue3'
import Layout from '@/Pages/Layout'
export default {
components: {
Link,
Layout,
},
data:function(){
return {
newTitle: '',
newUrl: '',
status: null,
cancelToken: null,
}
},
props:{
bookmarks: {
type: Array,
},
errors: {
type: Object,
}
},
computed: {
bookmarkError() {
return this.$page.props.errors['addBookmark'] || {}
},
},
methods: {
addBookmark: function(event){
this.$inertia.visit(route('bookmark.store'),{
method: 'post', //POSTメソッドで送信
data:{
title: this.newTitle, //送信データを指定
url: this.newUrl,
},
errorBag: 'addBookmark', //エラーバッグ名を指定
preserveState: function(page){ //フォームの内容を保持するかどうか
return Object.keys(page.props.errors).length != 0 //エラーがあればフォームの内容を保持する
},
preserveScroll: true, //スクロールポジションを維持する
onCancelToken: (cancelToken) => (this.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 => {this.cancelToken = null; console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される
});
},
}
}
</script>
<?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' => fn() => 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 = []);
}
}

Comments