Inertia Linkの使い方 Inertia.js入門 #5

Laravel

この記事では、Inertia.jsのLinkコンポーネントについて解説します。

この連載記事ではLaravelにおけるInertia.jsの使い方を解説しています。他の記事と合わせてお読みください。

Laraveler
Laraveler

InertiaのLinkコンポーネントとは一体何でしょうか?aタグのリンクではどうしてダメなんですか?

シップ
シップ

aタグだと通常のリクエストとなり、Inertia.jsの特徴であるSPAにならないため、リンクをXHRリクエストに変換してくれるInertia Linkを使用します。

Inertia Linkとは

通常のaタグによるリンクでは、ページ移動の度にすべてのデータを読み直すHTTPリクエストが発生します。Inertiaアプリの本髄は、初回にフルHTMLなデータを読み込んだ後は、次からのページアクセス時には必要なデータだけを読み込むSPAです。

InertiaのLinkコンポーネントは、aタグをクリックした際に、通常のHTTPリクエストを行う代わりに必要なデータだけをリクエストするXHRリクエストを行うための仕組みです。

Inertia.jsを使用する際には必ず使うことになるので使い方をしっかり覚えておきましょう。

Inertia Linkの作成

通常のaタグ

<a href="/">Home</a>

このような通常のaタグによるリンクをInertia linkに変更するには、Linkコンポーネントをインポートし、aタグをLinkタグに書き換えるだけです。

LinkコンポーネントはInertiaLinkコンポーネントのエイリアスです。

<Link href="/">Home</Link>

これで、クリック時にXHRリクエストに変更するイベントハンドラの設定されたaタグが出力されます。装飾などは通常のaタグとして行えます。

buttonリンク

aタグではなく、buttonタグでリンクを作成したい場合はas属性でbuttonを指定します。

<Link href="/logout" method="post" as="button" type="button">Logout</Link>

// これは次のようなタグが出力されます:
<button type="button">Logout</button>

Inertia Linkの属性

HTTPメソッド

Inertia LinkはデフォルトではGETリクエストを行いますが、method属性で他のメソッドを使用するよう指定することもできます。対応メソッドはGETの他、POST, PUT, PATCH, DELETEです。

//POSTメソッドでリクエストを行う場合
<Link href="/logout" method="post">Logout</Link>

Data属性

data属性で送信するリクエストに付加するデータを指定できます。data属性にはオブジェクトか、FormDataインスタンスを指定できます。

<Link href="/endpoint" method="post" :data="{ foo: bar }">Save</Link>

カスタムヘッダー

headers属性で、リクエストに追加するHTTPヘッダーを指定できます。Inertiaがデフォルトで付加するヘッダーは上書きできません。

<Link href="/endpoint" :headers="{ foo: bar }">Save</Link>

履歴管理

デフォルトでは、Inertiaリンクによるページ移動では、ブラウザの履歴に移動したページが追加されていき、戻ったり進んだりできるようになっています。

replace属性をつけることで、ページ移動の際に履歴を追加するのではなく、現在の履歴を書き換えるようにすることができます。

<Link href="/" replace>Home</Link>

同じURLへのリクエストの場合は履歴は追加されず、replace属性が付いた状態となります。

状態の保存

preserve-state属性をつけると、ページ移動時にフォームに入力した状態を維持させることができます。

<input v-model="query" type="text" />
<Link href="/search" :data="{ query }" preserve-state>Search</Link>

レイアウト機能でも、フォームの状態を保存することが出来ましたが、どう違うんですか?

シップ
シップ

レイアウト機能では、読み直す部分ではないページの状態が維持されましたが、preserve-stateでは読み込むページ自身にあるフォームの状態が維持されます。

スクロールの抑制

preserve-scroll属性をつけると、ページ移動時にスクロールポジションがリセットされなくなり、移動前と同じスクロール位置が維持されるようになります。

<Link href="/" preserve-scroll>Home</Link>

部分的なデータ読み込み

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

<Link href="/users?active=true" :only="['users']">Show active</Link>

サンプルをモデル使用版へ変更

このサンプルは前回から引き継いで使用しています。

前回のソースコードはこちらから→GitHub

モデル作成

前回まで作成していたブックマーク一覧を改良します。モデルを作成して実際にブックマークを追加できるようにします。

Dockerの場合は「make app」でAppコンテナに入ってからこのコマンドを入力してください
php artisan make:model Bookmark -ms

//実行結果
   INFO  Model [app/Models/Bookmark.php] created successfully.
   INFO  Migration [database/migrations/2023_08_15_105210_create_bookmarks_table.php] created successfully.
   INFO  Seeder [database/seeders/BookmarkSeeder.php] created successfully.

モデルのマイグレーションファイルを変更してtitleurlをカラムとして追加します。

//略・・・
    public function up()
    {
        Schema::create('bookmarks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('url');
            $table->timestamps();
        });
    }
//略・・・

保存したらマイグレーションを実行します。

php artisan migrate

//実行結果
   INFO  Running migrations.
  2023_08_15_105210_create_bookmarks_table ............................. 18ms DONE

シーダーの追加

あらかじめデータを挿入しておくためのシーダーを追加します。

<?php

namespace Database\Seeders;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Seeder;

class BookmarkSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('bookmarks')->insert([
            [
                'title' => 'SOHO MIND',
                'url' => 'https://blog.shipweb.jp/',
            ],
            [
                'title' => 'どこかにアンテナ',
                'url' => 'https://ant.shipweb.jp/',
            ],
        ]);
    }
}

シーディングを実行します。

php artisan db:seed --class=BookmarkSeeder

//実行結果
   INFO  Seeding database.

これでモデルの追加と初期データの追加が完了しました。次にコントローラーを変更していきます。

コントローラーの変更

コントローラーもモデルを使用する仕様に変更します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\Bookmark;

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) 
    {
        $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 = []);
    }
}

ルートの追加

この3つのルートを新たに追加します。検索と追加、削除用のルートです。

//下の3行を追加
Route::get('/Bookmark/search/{queryWord}',[App\Http\Controllers\BookmarkController::class, 'search'])->where('queryWord', '.*')->name('bookmark.search');
Route::post('/Bookmark/store',[App\Http\Controllers\BookmarkController::class, 'store'])->name('bookmark.store');
Route::delete('/Bookmark/delete/{id}',[App\Http\Controllers\BookmarkController::class, 'destroy'])->name('bookmark.delete');

Inertia Linkの実例

検索ボタンの変更

検索ボタンです。

  • as属性にbuttonを指定してボタンにします。
  • method属性にgetを指定してGETリクエストを行います。
  • :href属性でリンク先を指定します。名前付きルートを使用して引数に検索ワードを渡しています。
  • preserve-state属性で、フォームの内容がページ読み込み時に残るようにしています。
<input type=text name=search v-model="searchWord" size=15 class="p-1 m-1 text-sm" />
<Link as="button" method="get" :href="route('bookmark.search',{queryWord: searchWord})" preserve-state class="border border-gray-400 m-1 p-1 text-sm">ブックマーク検索</Link>

ブックマーク追加ボタン

ブックマーク追加するために、タイトルとURLを入力するボックスと追加ボタンを追加しました。
追加ボタンはこのような感じです。

  • method属性で、POSTリクエストにしています。
  • data属性で、送信するデータを指定しています。titleに新規タイトル、urlに新規URLを渡しています。
<Link as="button" method="post" :href="route('bookmark.store')" :data="{title: newTitle, url: newUrl}" class="border border-gray-400 m-1 p-1 text-sm">ブックマーク追加</Link>

ブックマーク削除ボタン

各ブックマークのリストの右側にブックマーク削除用のボタンを追加しました。

  • method属性で、DELETEリクエストにしています。
  • only属性をつけて、応答データとして「bookmarks」だけを読み込むように指定しています。
<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>

only属性をつけた時の実際の挙動

only属性をつけると部分的なデータを読み込むというのがいまいちピンとこない方のために、only属性をつけた場合と付けない場合とでどう変わるのかをデベロッパーツールで見てみます。

only属性をつけない通常のリクエスト。bookmarks以外にもjetstreamやuserなどのデータが読み込まれています。
only属性をつけるとpropsで渡されるデータは先ほどあったjetstreamなどは渡されずbookmarksだけになります。

このように、only属性をつけると通信量の節約になります。ただこれだけではLaravel側でデータは取得しているので、サーバー側の負荷は変わっていません。データ自体を取得しないようにするにはコントローラー側でもう一工夫必要になります。

//これだと毎回取得してしまう
return Inertia::render('Bookmark/Index',['bookmarks' => Bookmark::all()]);

//これで必要な時だけ取得するようになる
return Inertia::render('Bookmark/Index',['bookmarks' => fn() => Bookmark::all()]);

サンプルソース

削除や追加ボタンを加えた「Index.vue」はこのようになります。

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

defineProps({ bookmarks: Array })
const newTitle = ref('');
const newUrl = ref('');
</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-60">
            <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>
        <div class="flex w-60">
            <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>
        <Link as="button" method="post" :href="route('bookmark.store')" :data="{title: newTitle, url: newUrl}" class="border border-gray-400 m-1 p-1 text-sm">ブックマーク追加</Link>
    </Layout>
</template>

「Layout.vue」はこのようになります。

<script setup>
import { ref } from "vue";
import { Link, Head } from '@inertiajs/vue3'

defineProps({ title: String })
const searchWord = ref('');
</script>

<template>
    <Head :title="title" />
    <main>
        <header class="bg-gray-100">
            <Link href="/" class="text-blue-700 underline m-2">Home</Link>
            <Link href="/HelloWorld" class="text-blue-700 underline m-2">HelloWorld</Link>
            <Link :href="route('bookmark.index')" class="text-blue-700 underline m-2">ブックマーク一覧</Link>
            <input type=text name=search v-model="searchWord" size=15 class="p-1 m-1 text-sm" />
            <Link as="button" method="get" :href="route('bookmark.search',{queryWord: searchWord})" preserve-state class="border border-gray-400 m-1 p-1 text-sm">ブックマーク検索</Link>
        </header>
        <article class="max-w-screen-sm">
            <slot />
        </article>
    </main>
</template>

完全なソース一覧はGitHubからご覧ください。

表示例
今回のまとめ
  • Inertia-linkはaタグを拡張する
  • 各種の属性をつけることで、追加のデータを渡したり、動作を変更できる。

次回はリンク以外からでもInertiaリクエストを発行するためのInertia.visit()を取り上げます。

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

タイトルとURLをコピーしました