Laravel+React(or Vue)を学習し始めた人の中で”React(or Vue)をLaravelと一緒に利用する方法が複数あるみたいだけど違いがわからない”という疑問を持っている人も多いのではないでしょうか。本文書ではLaravel環境でReact(or Vue)を利用するための方法を本ブログの公開済みの記事と共にに紹介し、その後Inertia.js+Reactを利用した方法について基礎から詳細に説明を行っています。Inertia.jsを利用するためにはReactの知識が必要となるのでReactがわからない人はInertis.jsの前にReactを学習することをお勧めします。

本文書はInertia.js+ReactですがInertia.js+Vue.jsを利用した場合の文書は公開済みです。

LaravelでReact(or Vue)を利用する方法

Laravelと一緒にReact(or Vue)を利用したい場合、主に3つの方法が考えられます。

(1)React(or Vue)をLaravelとは別環境にインストールし、LaravelをバックエンドのAPIサーバとして利用する方法

LaravelではフロントエンドにReactのフレームワークNext.jsを利用したパッケージを公開しています。Laravelとは別環境でフロントエンドの開発を行うのでフロントエンド側はNext.jsに限らずVueのフレームワークのNuxt.js、フレームワークを利用しないReact/Vue/Svelte etc.でも利用することができます。

(2)Laravel環境下にInertia.jsとReact(or Vue)をインストールして利用する方法

Laravel Breeze, Laravel Jetstreamパッケージを利用することで特別な追加設定を行うことなくReact(or Vue)を利用することができます。Inertia.jsがサポートしているのは現在Vue, React, Svelteです。React(or Vue)とLaravelとのデータの送受信はInertia.jsを介して行います。この構成ではInertia.jsの理解が必要となります。

(3)Laravel環境下にReact(or Vue)をインストールして利用する方法(Inertia.jsは利用しない)

Laravel環境下にReact(or Vue)のインストールを行い、BladeファイルからコンパイルしたReact(or Vue)ファイルを読み込むことで利用することができます。Laravel7まではLaravel UIパッケージ(認証用パッケージ)を利用する方法としてドキュメントに記載されていました。Laravel9ではLarave UIパッケージをインストールするかパッケージを利用することなくReact(or Vue)をLaravelと同じ環境にインストールして利用することができます。ルーティングファイルを適切に設定することでReact Routerを利用してSPA(Single Page Application)を作成することもできます。

現行バージョン(執筆時は9)のLaravelのドキュメントに記載されているのは(1), (2)の方法です。本文書では(2)のLaravel環境下にInertia.jsを利用する方法について説明を行っています。Laravel BreezeやLaravel Jetstreamを利用することでInertia.js + Reactを利用することができますが本文書ではパッケージを利用しない方法を中心に解説しています。

macOSにLaravel 9をインストールして動作確認を行っています。

Laravelプロジェクトの作成

Inertia.jsをインストールする前にLaravelプロジェクトを作成する必要があります。Inertia.jsではJavaScriptを利用するため事前にNode.jsとnpmがインストールされている必要があります。

本文章ではLaravelプロジェクトの作成はlaravel newコマンドを利用して行います。プロジェクト名にはlaravel_inertiaとしていますが任意の名前をつけてください。


 % laravel new laravel_inertia

プロジェクトの作成後、プロジェクトフォルダlaravel_inertiaに移動してphp artisan serveコマンドを実行して開発サーバは起動します。


 % cd laravel_inertia
 % php artisan serve
Starting Laravel development server: http://127.0.0.1:8000
[Wed Apr  6 22:23:37 2022] PHP 8.0.7 Development Server (http://127.0.0.1:8000) started

ブラウザでアクセスするとLaravelの初期画面が表示されます。

Laravel9の初期画面
Laravel9の初期画面

データベースの設定

動作確認用なのでデータベースには簡易的に利用することができるSQLiteデータベースを利用します。

touchコマンドでdatabaseフォルダにデータベースファイルdatabase.sqliteを作成します。


 % touch database/database.sqlite

ファイルを作成後.envファイルでSQLiteへの接続設定を行います。環境変数DB_CONNECTIONにはデフォルトではmysqlが設定されていますがsqliteに変更します。DB_CONNECTION以外の.envファイルにある先頭にDB_がつく環境変数をすべて削除してください。

削除後migrationsフォルダにあるテーブル作成のマイグレーションファイルを利用してテーブルを作成するためにphp artisan migrateコマンドを実行します。


 % php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (3.62ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (2.32ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (1.96ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (2.77ms)

作成したusersテーブルにダミーデータを挿入するためdatabase¥seedersフォルダにあるDatabaseSeeder.phpファイルを開いてrunメソッドにあるコメントを外します。factoryメソッドの引数の10が挿入するデータ件数を表しています。


public function run()
{
    \App\Models\User::factory(10)->create();
}

10件分のダミーデータを挿入するためphp artisan db:seedコマンドを実行します。


 % php artisan db:seed
Database seeding completed successfully.

tinkerコマンドを利用してusersテーブルにデータが挿入されているか確認しておきます。


 % php artisan tinker
Psy Shell v0.11.2 (PHP 8.0.7 — cli) by Justin Hileman
>>> User::all();
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
=> Illuminate\Database\Eloquent\Collection {#4452
     all: [
       App\Models\User {#4454
         id: "1",
         name: "Octavia Kuphal",
         email: "joel81@example.com"

ダミーデータの挿入が確認できればLaravelの環境の構築は完了です。

Inertia.jsのインストールと設定

Inertia.jsはLaravel BreezeまたはLaravel Jetstreamをインストールすることで利用することができますが理解を深めるためにここではcomposerコマンドを利用してInertia.jsのパッケージをインストールします。

インストールを含めInertia.jsについてはLaravelのドキュメントではなくInertia.jsのドキュメントで確認する必要があります。インストールはサーバサイド用(Laravel側)のパッケージとクライアント側(ReactとVueでは異なるパッケージ)で別々のパッケージをインストールします。

サーバサイドの設定

最初にサーバサイド側のパッケージをインストールします。インストールにはcomposerコマンドを利用します。


 % composer require inertiajs/inertia-laravel 

インストールが完了したらresources¥viewsフォルダにapp.blade.phpファイルを作成してInertia.jsのドキュメントのRoot templateに記載されているコードをコピー&ペーストします。Inertia.jsのデフォルトの設定でappファイルを利用するように設定されているためapp.blade.phpファイルが必要となります。app.blade.phpファイルではコンパイルされたJavaScriptファイルを読み込みとInert.jsの設定(@inertia)が行われています。app.blade.phpを利用する設定は後ほどインストールを行うmiddlewareのHandleInertiaRequestsに設定されています。

root templateの設定
root templateの設定

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
    <script src="{{ mix('/js/app.js') }}" defer></script>
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>

次にinertiaのmiddlewareのインストールを行います。php artisan inertia:middlewareコマンドを実行するとHttp¥MiddlewareのフォルダにHandleInertiaRequests.phpファイルが作成されます。


 % php artisan inertia:middleware             
Middleware created successfully.

インストール後にHandleInertiaRequests middlewareをapp¥Http¥Kernel.phpファイルに追加する必要があります。


protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\HandleInertiaRequests::class, //追加
    ],

middlewareの追加をKernel.phpファイルに行うとリクエストを行う度にHandleInertiaRequestsファイルが実行されることになります。

クライアントサイドの設定

Reactを利用する場合は下記のパッケージをインストールします。npmコマンドまたはyarnコマンドを利用してインストールを行います。


 % npm install @inertiajs/inertia @inertiajs/inertia-react 
or 
 % yarn add @inertiajs/inertia @inertiajs/inertia-react

reactとreact-domのインストールを行います。inertia.jsの依存関係で最新版のバージョン18はエラーが表示されるのでバージョン17をインストールしています。


 % npm install react@17 react-dom@17

Reactのインストール完了後、resource¥js¥app.jsファイルにReactの設定を追加します。設定はInertia.jsのドキュメントに記載されているものを利用します。


require("./bootstrap");

import React from "react";
import { render } from "react-dom";
import { createInertiaApp } from "@inertiajs/inertia-react";

createInertiaApp({
    resolve: (name) => require(`./Pages/${name}`),
    setup({ el, App, props }) {
        render(<App {...props} />, el);
    },
});

インストールしたReactを利用するためにLaravel mixの設定ファイルであるwebpack.mix.jsファイルにreact()を追加します。Laravel mixはwebpackを利用しています。


mix.js("resources/js/app.js", "public/js")
    .react()
    .postCss("resources/css/app.css", "public/css", [
        //
    ]);

npm run watchコマンドを実行してJavaScriptファイルをビルドします。


 % npm run watch

> watch
> mix watch

        Additional dependencies must be installed. This will only take a moment.
 
        Running: npm install @babel/preset-react --save-dev --legacy-peer-deps

追加で@babel/preset-reactをインストールするようにメッセージが表示されるので@babel/preset-reactのインストールを行います。


 % npm install @babel/preset-react --save-dev --legacy-peer-deps

再度npm run watchコマンドを実行します。


 % npm run watch
 //略
ERROR in ./resources/js/app.js 15:11-43
Module not found: Error: Can't resolve './Pages' in '/Users/mac/Desktop/laravel_inertia/resources/js'

webpack compiled with 1 error

app.jsファイルで設定しているPagesフォルダが存在しないためエラーになっているのでjsフォルダの下にPagesフォルダを作成します。npm run watchコマンドを実行しているのでPagesフォルダを作成した瞬間にエラーは解消しビルドは成功します。

Pagesフォルダの中にWelcome.jsxファイルを作成します。


import { useEffect } from "react";

const Welcome = () => {
    useEffect(() => {
        console.log("Welcome Page mounted");
    }, []);

    return <h1>Welcome Inertia.js</h1>;
};

export default Welcome;

Pagesフォルダに作成したWelcome.jsxファイルの内容が表示されるようにweb.phpファイルにルーティングを設定します。Inertiaクラスのrender関数の引数に作成したWelcome.jsxファイルを指定します。


<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

//略

Route::get('/', function () {
    return Inertia::render('Welcome');
});

php artisan servコマンドを実行してブラウザでアクセスすると”Welcome Inertia.js”の文字列が表示されます。デベロッパーツールのコンソールには”Welcome Page mounted”のメッセージが表示されます。

Welcome.vueファイルの内容が表示
Welcome.vueファイルの内容が表示

Inertia.jsでの設定はBladeファイルの内容を表示する際に利用できるView::make()に似ています。View::make()のヘルパー関数のview()があるようにInertia::render()のヘルパー関数にinertia()があります。


Route::get('/', function () {
    return inertia('Welcome');
});

Chromeを利用している場合はエクステンションのReact Developer Toolsを利用することでどのような情報が含まれているか確認することができます。

React Developer Toolsによる確認
React Developer Toolsによる確認

Inertia.jsの動作確認

Inertia.jsの初期設定と動作確認が確認ができたのでここからInertia.jsを利用する上で必要となる機能を確認していきます。

コンポーネントにデータを渡す

ページコンポーネントにデータを渡す際には下記のように行うことができます。greetingを使って文字列Helloを渡しています。Bladeファイルのデータを渡す時と同じ設定方法です。


Route::get('/', function () {
    return Inertia::render('Welcome',[
        'greeting' => 'Hello'
    ]);
});

React Developer Toolsを確認すると設定した値はpropsの中にgreetingとその値が確認できます。

Devtoolsでの値の確認
Devtoolsでの値の確認

propsの中に含まれているgreeingの値を関数の引数から渡されるpropsを使って取得します。ページコンポーネントに渡したデータはReactのpropsとして扱えることがわかります。


import { useEffect } from "react";

const Welcome = (props) => {
    useEffect(() => {
        console.log("Welcome Page mounted");
    }, []);

    return <h1>{props.greeting} Inertia.js</h1>;
};

export default Welcome;

ブラウザで確認するとpropsで渡された”Hello”が表示されています。

propsから渡された値を表示
propsから渡された値を表示

propsは分割代入を利用して記述することもできます。


import { useEffect } from "react";

const Welcome = ({greeting}) => {
    useEffect(() => {
        console.log("Welcome Page mounted");
    }, []);

    return <h1>{greeting} Inertia.js</h1>;
};

export default Welcome;

ページの作成とページ遷移

複数のページを作成しページを移動するための設定方法を確認します。

Pagesフォルダに新たにAbout.jsxファイルを作成します。


const About = () => {
    return <h1>About</h1>;
};

export default About;

作成したAbout.jsxファイルの内容を表示するためにweb.phpファイルに/aboutへのルーティングを追加します。


Route::get('/about', function () {
    return Inertia::render('About');
});

/aboutにアクセスすると”About”の文字列が表示されます。

Aboutページの表示
Aboutページの表示

WelcomeページとAboutページの2つのページを作成することができたのでページ間を移動できるようにリンクを設定します。aタグを利用してリンクを設定します。下記はAbout.jsxファイルのコードですが同じ設定をWelcome.jsxでも行ってください。


const About = () => {
    return (
        <>
            <nav>
                <ul>
                    <li>
                        <a href="/">Welcome</a>
                    </li>
                    <li>
                        <a href="/about">About</a>
                    </li>
                </ul>
            </nav>
            <h1>About</h1>
        </>
    );
};

export default About;

ページ上部にリンクが表示されます。

aタグによるページの移動
aタグによるページの移動

ページを移動することはできますがページを移動するたびにページ全体が読み込まれていることがわかります。

Inertia.jsはLinkコンポーネントを持っているのでaタグの代わりに利用することができます。


import { Link } from "@inertiajs/inertia-react";
const About = () => {
    return (
        <>
            <nav>
                <ul>
                    <li>
                        <Link href="/">Welcome</Link>
                    </li>
                    <li>
                        <Link href="/about">About</Link>
                    </li>
                </ul>
            </nav>
            <h1>About</h1>
        </>
    );
};

export default About;

aタグからLinkコンポーネントに変更後はページを移動する度にページ全体が読み込まれるのではなく更新が必要な場所のみ更新されるようになるためスムーズにページの移動を行うことができるようになります。

コンポーネント化

Welcome.jsx, About.jsxの2つのファイルに各ページのリンクのnavタグを追加しましたがコードは共通なのでコンポーネント化します。

jsフォルダに新たにComponentsフォルダを作成します。NavBar.jsxファイルを作成して下記のリンクを記述します。コンポーネントの作成についてはReactの通常の方法と同じです。


import { Link } from "@inertiajs/inertia-react";
const NavBar = () => {
    return (
        <nav>
            <ul>
                <li>
                    <Link href="/">Welcome</Link>
                </li>
                <li>
                    <Link href="/about">About</Link>
                </li>
            </ul>
        </nav>
    );
};

export default NavBar;

Welcome.jsx, About.jsxでは作成したNavBarをimportして利用します。About.jsxファイルだけではなくWelcome.jsxファイルも更新してください。navタグの部分をコンポーネント化しても表示に変化はありません。


import NavBar from "../Components/NavBar";

const About = () => {
    return (
        <>
            <NavBar />
            <h1>About</h1>
        </>
    );
};

export default About;

名前付きルートの設定(Ziggy)

Linkのhref属性にはURL(/, /about)を記述していましたがLaravelではヘルパー関数のrouteの引数にルーティングで設定した名前を指定することで名前からURLを設定することができます。

ヘルパー関数のrouteを利用するためにはルーティングへ名前の設定を行っておく必要があります。nameメソッドを利用して/(ルート)のルーティングにはwelcome, /aboutのルーティングにはaboutと名前をつけています。


Route::get('/', function () {
    return Inertia::render('Welcome',[
        'greeting' => 'Hello'
    ]);
})->name('welcome');

Route::get('/about', function () {
    return Inertia::render('About');
})->name('about');

NavBarコンポーネントに設定しているLinkのhref属性にv-bindディレクティブを設定しroute関数の引数にはweb.phpファイルのルーティングで設定した名前を設定します。


import { Link } from "@inertiajs/inertia-react";
const NavBar = () => {
    return (
        <nav>
            <ul>
                <li>
                    <Link href={route("welcome")}>Welcome</Link>
                </li>
                <li>
                    <Link href={route("about")}>About</Link>
                </li>
            </ul>
        </nav>
    );
};

export default NavBar;

設定後にブラウザで確認するとデベロッパーツールのコンソールにメッセージ(Uncaught ReferenceError: route is not defined)が表示され動作しません。Laravelのヘルパー関数routeはPHPの関数なのでBladeファイル上でのみ利用することができます。

React(JavaScript)上でヘルパー関数のrouteを利用するためにZiggyパッケージをインストールして設定を行う必要があります。

Ziggy/TIGHTEN
Ziggy/TIGHTEN

composerコマンドでZiggyパッケージのインストールを行います。


 % composer require tightenco/ziggy

Ziggyのドキュメントにはインストール後の設定について”Add the @routes Blade directive to your main layout (before your application’s JavaScript), and the route() helper function will now be available globally!”と記述されているのでapp.blade.phpファイルに@routesディレクティブを追加します。


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
    <script src="{{ mix('/js/app.js') }}" defer></script>
    @inertiaHead
    @routes
  </head>
  <body>
    @inertia
  </body>
</html>

ここまででZiggyの設定は完了です。設定後はroute関数を設定する前と同様にページの移動が可能となります。

名前付きルートを設定するメリットは後ほどアプリケーション設計の変更によりURLがaboutから/company/aboutに変更になったとしてもLinkのhrefの設定を変更する必要がないことです。URLでhrefを設定している場合はリンクが設定されているすべてのページで/aboutを/company/aboutに変更する必要があります。

@routesディレクティブを追加することでブラウザからソースコードを確認するとZippy変数が設定されていることが確認できます。

ソースにより@routesの確認
ソースにより@routesの確認

コントローラーからの画面表示

これまではルーティングファイルweb.phpファイルから直接Inertiaのrender関数を実行していましたがコントローラーファイルからrender関数を実行するためUserController.phpファイルを作成します。


 % php artisan make:controller UserController
Controller created successfully.

コマンドを実行するとapp¥Http¥Controllersの下にUserController.phpファイルが作成されます。作成されたUserController.phpファイルにindexメソッドを追加します。


<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    public function index(){
        return Inertia::render('User/Index',[
            'users' => User::all()
        ]);
    }
}

render関数の引数に設定したUser/IndexはPagesフォルダの下のUserフォルダのIndex.jsxファイルを示しています。UserフォルダもIndex.jsxファイルも存在しないので作成します。Index.jsxファイルに対してUserモデルを利用して取得したユーザ情報をusers変数で渡しています。

web.phpファイルにルーティングを追加します。


use App\Http\Controllers\UserController;
//略
Route::get('/user', [UserController::class, 'index'])->name('user');

UserフォルダのIndex.jsxファイルではusers変数をpropsで受け取ることができます。受け取ったusers変数をmap関数を利用して展開して表示しています。


const Index = (props) => {
    return (
        <>
            <h1>ユーザ一覧</h1>
            <ul>
                {props.users.map((user) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </>
    );
};

export default Index;

追加した/userにブラウザからアクセスすると画面上にはユーザ名の一覧が表示されます。

ユーザ一覧の表示
ユーザ一覧の表示

このようにInertia.jsではコントローラーの中でUserモデルを介してデータベースから取得したデータをpropsでjsxファイルに渡し表示することができます。

作成したユーザページへのリンクをNavBarコンポーネントに追加します。


<ul>
    <li>
        <Link href={route("welcome")}>Welcome</Link>
    </li>
    <li>
        <Link href={route("about")}>About</Link>
    </li>
    <li>
        <Link href={route("user")}>User</Link>
    </li>
</ul>

User¥Index.jsxファイルにもNavBarをimportします。


import NavBar from "../../Components/NavBar";
const Index = (props) => {
    return (
        <>
            <NavBar />
            <h1>ユーザ一覧</h1>
            <ul>
                {props.users.map((user) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </>
    );
};

export default Index;

Inertiaを利用しない場合

Inertia.jsを利用しない場合のユーザ情報の取得方法について確認しておきましょう。Inertia.jsを利用しない場合はuseState, useEffectのReact Hooksを利用します。


import { useState, useEffect } from "react";
import NavBar from "../../Components/NavBar";
const Index = () => {
    const [users, setUsers] = useState([]);

    useEffect(() => {
        axios.get("/api/user").then((response) => setUsers(response.data));
    }, []);

    return (
        <>
            <NavBar />
            <h1>ユーザ一覧</h1>
            <ul>
                {users.map((user) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </>
    );
};

export default Index;

APIのルーティングファイルapi.phpに/userのルーティングを追加します。


Route::get('/user',function(){
    return User::all();
});

どちらも表示される結果は同じですがInertia.jsを利用した場合と利用しない場合では設定する内容が異なることが理解できるかと思います。Inertia.jsを利用する場合はコントロールファイルにInertiaのrender関数を設定してpropsでデータを渡すのでBladeファイルを利用した方法のようにLaravelと密接な関係を持っていることがわかります。

Activeステータスの設定

3つのページのリンクが表示されていますがリンクを見ても現在どのページを表示しているかはわかりません。リンクを見ることで現在どのページを表示しているかわかるように設定を行っていきます。

表示しているページのURLとページコンポーネントの情報はusePage関数を利用して取得することができます。この情報を利用することで現在表示しているページのリンクにclassを適用します。

最初にNavBarコンポーネントを使ってusePage関数の動作確認を行います。


import { Link } from "@inertiajs/inertia-react";
import { usePage } from "@inertiajs/inertia-react";

const NavBar = () => {
    const { url, component } = usePage();
    return (
        <>
            <div>URL: {url}</div>
            <div>Component: {component}</div>
            <nav>
                <ul>
                    <li>
                        <Link href={route("welcome")}>Welcome</Link>
                    </li>
                    <li>
                        <Link href={route("about")}>About</Link>
                    </li>
                    <li>
                        <Link href={route("user")}>User</Link>
                    </li>
                </ul>
            </nav>
        </>
    );
};

export default NavBar;

ブラウザ上には表示しているURLとページコンポーネントを確認することができます。

URLとComponentを確認
URLとComponentを確認

URLとコンポーネントの情報が取得できたのでどちらかの情報を利用して表示しているページのリンクにclassを設定することができます。

Userページを表示している時にリンクをネイビーの太文字で表示できるようにactiveクラスを適用します。activeクラスはComponentsフォルダにNavBar.cssファイルを作成し追加します。


.active {
    font-weight: bold;
    color: navy;
    text-decoration: none;
}

usePage関数から取得したurlを利用してurlと/userが一致する場合はactiveクラスを適用し一致しない場合にはクラスを適用しません。


import "./NavBar.css";
//略
<li>
    <Link
        href={route("user")}
        className={url === "/user" ? "active" : ""}
    >
        User
    </Link>
</li>

ブラウザで確認するとUserページを表示している時はリンクのUserの文字列がネイビーで太文字になることが確認できます。他のページに移動するとactiveクラスは適用されないので元の文字に戻ります。

activeクラスの適用
activeクラスの適用

他のリンクにも同様の設定を行います。


<ul>
    <li>
        <Link
            href={route("welcome")}
            className={url === "/" ? "active" : ""}
        >
            Welcome
        </Link>
    </li>
    <li>
        <Link
            href={route("about")}
            className={url === "/about" ? "active" : ""}
        >
            About
        </Link>
    </li>
    <li>
        <Link
            href={route("user")}
            className={url === "/user" ? "active" : ""}
        >
            User
        </Link>
    </li>
</ul>

URLではなくコンポーネントの情報を利用したい場合には下記のように行うことができます。


<Link
    href={route("user")}
    className={
        component === "User/Index" ? "active" : ""
    }
>
    User
</Link>

現在のリンクの設定ではページを移動すると現在表示しているページのリンクにactiveクラスが適用されます。

しかしユーザ一覧ではなく個別のユーザの情報をアクセスするために/user/1というような場合には1という値が固定値ではなくなるため今回の設定では対応することができます。そのような場合はstartsWithメソッドを利用して先頭の文字列が一致するかの条件でactiveクラスを適用するかを判断します。


<Link
    href={route("user")}
    className={url.startsWith("/user") ? "active" : ""}
>
    User
</Link>

レイアウトの設定

レイアウトの設定を行うことで複数のページで同じレイアウトを適用することができます。レイアウトの変更が必要な場合は1つのレイアウトファイルを更新するだけなのでメンテナンスが楽になります。

resouces¥jsフォルダの下にLayoutsフォルダを作成してLayout.jsxファイルを作成します。Layoutファイルの中ではNavBarをimportして利用します。Layoutファイルを利用するページコンポーネントの中身を表示するためにchildrenを利用します。


import NavBar from "../Components/NavBar";

const Layout = ({ children }) => {
    return (
        <>
            <NavBar />
            {children}
        </>
    );
};

export default Layout;

About.jsxファイルをLayoutコンポーネントを利用して書き換えます。表示する内容はLayoutタグの中に挿入します。


import Layout from "../Layouts/Layout";

const About = () => {
    return (
        <Layout>
            <h1>About</h1>
        </Layout>
    );
};

export default About;

Welcome.jsx、User¥Index.jsxファイルでも同様にLaytoutコンポーネントをimportして設定を行ってください。

Layout.jsxファイルとNavBar.jsxファイルにstyle属性、class属性を追加してスタイルを設定します。


import NavBar from "../Components/NavBar";

const Layout = ({ children }) => {
    return (
        <>
            <header>
                <h1 style={{ textAlign: "center" }}>Logo</h1>
                <NavBar />
            </header>
            <main>{children}</main>
        </>
    );
};

export default Layout;

NavBar.jsxにはul要素にmenuクラスと適用しています。flexを利用してメニューの表示を横並びにしています。


import { Link } from "@inertiajs/inertia-react";
import { usePage } from "@inertiajs/inertia-react";
import "./NavBar.css";

const NavBar = () => {
    const { url } = usePage();
    return (
        <>
            <nav>
                <ul className="menu">
                    <li>
                        <Link
                            href={route("welcome")}
                            className={url === "/" ? "active" : ""}
                        >
                            Welcome
                        </Link>
                    </li>
                    <li>
                        <Link
                            href={route("about")}
                            className={url === "/about" ? "active" : ""}
                        >
                            About
                        </Link>
                    </li>
                    <li>
                        <Link
                            href={route("user")}
                            className={url === "/user" ? "active" : ""}
                        >
                            User
                        </Link>
                    </li>
                </ul>
            </nav>
        </>
    );
};

export default NavBar;

menuクラスはimportしているNavBar.cssに追加します。


.menu {
    display: flex;
    justify-content: space-around;
    list-style-type: none;
}

ブラウザを確認するとWelocme, About, UserページのヘッダーにLogoの文字列と他のページへ移動するためのナビゲーションが表示されます。

レイアウトファイルの適用
レイアウトファイル適用後のUserページ

Persistent Layoutsの設定

Layoutファイルの設定を行うことができましたが現在の設定ではページを移動するためにLayoutコンポーネントのマウント、アンマウントが行われます。

マウント、アンマウントが行われているのか確認するためにLayout.jsxファイルにuseEffect Hookを追加します。


import { useEffect } from "react";
import NavBar from "../Components/NavBar";

const Layout = ({ children }) => {
    useEffect(() => {
        console.log("Mount Layout");
        return () => {
            console.log("UnMount Layout");
        };
    }, []);
    return (
        <>
            <header>
                <h1 style={{ textAlign: "center" }}>Logo</h1>
                <NavBar />
            </header>
            <main>{children}</main>
        </>
    );
};

export default Layout;

設定後ページを移動するとページを移動するたびにブラウザのデベロッパーツールのコンソールに”UnMount Layout”と”Mount Layout”が表示されます。

後ほどPersistent Layoutsとの比較を見るためにReact developer Toolsを利用してコンポーネントの情報を確認しておきます。

Aboutコンポーネントの内側にLayoutコンポーネントが含まれていることが確認できます。

developer toolsによる確認
developer toolsによる確認

About.jsxファイルでPersistent Layoutsの設定を行います。Persistent Layoutsを利用した場合はLayoutタグをAbout関数の中から削除することができます。


import Layout from "../Layouts/Layout";

const About = () => {
    return <h1>About</h1>;
};

About.layout = (page) => <Layout children={page} />;

export default About;

設定後表示される内容は変わりませんがにReact developer Toolsを利用すると先ほどまではAboutタグの中に入っていたLayoutタグがAboutタグの外側に表示されていることが確認できます。

React Developer ToolsによるLayoutの確認
React Developer ToolsによるLayoutの確認

Welcome.jsx、User/Index.jsxファイルでも同様にPersistent Layoutsの設定を行います。

すべてのファイルにPersistent Layoutsを設定後、ページの移動を行ってください。最初のアクセス時にはコンソールに”Mount Layout”が表示されますがページを移動しても先ほどのように”UnMount Layout”、”Mount Layout”のメッセージが表示されることはありません。Layoutコンポーネントがマウント、アンマウントされることなく保持されていることがわかります。

Title設定

デフォルトではtitleタグに何も設定を行っていないためブラウザタブを確認するとURLが表示されています。

ブラウザのタグに表示されている内容の確認
ブラウザのタグに表示されている内容の確認

titleを設定したい場合にはHeadコンポーネントを利用することができます。


import Layout from "../Layouts/Layout";
import { Head } from "@inertiajs/inertia-react";

const About = () => {
    return (
        <>
            <Head>
                <title>Aboutページ</title>
            </Head>
            <h1>About</h1>
        </>
    );
};

About.layout = (page) => <Layout children={page} />;

export default About;

Headコンポーネントタグの間にtitleタグを挿入することでtitleタグの間に入れた内容がheadタグのtitleタグに設定されます。デベロッパーツールで要素の情報を確認するとtitleタグの中にはinertiaという文字列を確認することもできます。

titleの確認
titleの確認

短縮系として下記のように記述することもできます。


<Head title="Aboutページ" />

About.jsxファイルでHeadタグを利用することでtitleを設定することができました。About.jsxファイルのように個別にtitleを設定することができますが複数のページに一括で設定したい場合にはLayoutファイルを利用することができます。Layout.jsxファイルでもAbout.jsxで設定した方法と同じ方法でtitleの設定を行います。


import NavBar from "../Components/NavBar";
import { Head } from "@inertiajs/inertia-react";

const Layout = ({ children }) => {
    return (
        <>
            <Head>
                <title>My App</title>
            </Head>
            <header>
                <h1 style={{ textAlign: "center" }}>Logo</h1>
                <NavBar />
            </header>
            <main>{children}</main>
        </>
    );
};

export default Layout;

Layout.jsxファイルを利用しているWelcome.jsxファイルではHeadコンポーネントを利用していないためtitleには”My App”が設定されます。About.vueファイルではHeadコンポーネントを利用してファイル内でtitleが設定されているためLayout.jsxファイルのtitleが上書きされてAbout.jsxファイルで設定した”Aboutページ”が設定されます。

本ブログでもタイトルには”ページのタイトル | アールエフェクト”というように文字数が許す限りタイトルの最後に” | アールエフェクト”を追加しています。同じ文字列を繰り返しページに設定するのを避けるためapp.jsのcreateInertiaAppのtitleを利用することができます。


createInertiaApp({
    resolve: (name) => require(`./Pages/${name}`),
    setup({ el, App, props }) {
        render(<App {...props} />, el);
    },
    title: (title) => `${title} | アールエフェクト`,
});

設定後Aboutページを確認するとタイトルにapp.jsファイルで設定したtitleで設定した関数の引数のtitleにはHeadコンポーネントで設定したtitleの値が設定されることがわかります。

titleの確認
titleの確認

WelcomeページにはHeadコンポーネントを設定していないのでLayoutコンポーネントで設定した値が設定されます。

Layout.vueファイルのtitleが設定
Layout.vueファイルのtitleが設定

Layout.jsxファイルのHeadコンポーネントを削除した場合にはWelcomeページでは” | アールエフェクト”が表示されると思うかもしれせんがWelcomeページにHeadコンポーネントが設定されていない場合にはデフォルトのURL(127.0.0.1:8000)が表示されます。titleを設定するためにHeadコンポーネントを設定しておく必要があります。

metaの設定

metaタグを設定する場合にもHeadコンポーネントを利用することができます。


import Layout from "../Layouts/Layout";
import { Head } from "@inertiajs/inertia-react";

const About = () => {
    return (
        <>
            <Head>
                <title>Aboutページ</title>
                <meta name="description" content="これはAboutページです" />
                <meta name="keywords" content="About" />
            </Head>
            <h1>About</h1>
        </>
    );
};

About.layout = (page) => <Layout children={page} />

metaタグのdescriptionとkeywordsをAbout.jsxファイルに設定後ブラウザで確認すると設定したmetaタグのdescriptionとkeywordsを確認することができます。。

metaタグの確認
metaタグの確認

Layout.jsxファイルでdescription, keywordsを設定した場合にtitleと同様に上書きされるのか確認します。

ブラウザで確認するとtitleは1つしか設定されていませんがdescriptionとkeywordsについては2つ登録されていることが確認できます。

metaタグは上書きされない
metaタグは上書きされない

metaタグが複数個登録されるのを防ぐためにhead-key属性があります。Layout.jsxファイルとAbout.jsxファイルのどちらのmetaタグにもhead-key属性を追加します。値にはmetaタグのnameの値を設定します。


<Head>
    <title>My App</title>
    <meta
        head-key="description"
        name="description"
        content="My Appアプリケーション"
    />
    <meta head-key="keywords" name="keywords" content="My App" />
</Head>

About.jsxファイルにも設定を行います。


<Head>
    <title>Aboutページ</title>
    <meta
        head-key="description"
        name="description"
        content="これはAboutページです"
    />
    <meta head-key="keywords" name="keywords" content="About" />
</Head>

設定後はAbout.jsxのmetaタグの設定のみ表示されていることが確認できます。またinertia属性の値にはhead-keyで設定した値が確認できます。

head-key属性を設定後のmetaタグ
head-key属性を設定後のmetaタグ

Layout.jsxファイルとAbout.jsxファイルのhead-keyが異なる場合はどちらのタグも表示されます。head-keyが同じであればhead-keyの値には任意の名前を設定することができます。mateタグのdescriptionを設定している場合にはhead-keyの値を必ずdescriptionに設定する必要はありません。

Formsの設定

Inertia.jsにおけるフォームの作成方法について確認していきます。

前準備

usersテーブルにユーザを追加するための入力フォームを作成するため入力フォーム用のページを作成します。resources¥js¥Pages¥Userの下にCreate.jsxファイルを作成します。/user/createでアクセスした場合にCreate.jsxファイルの内容が表示されるようにルーティングの変更を行います。

現在のルーティング設定はgetリクエストのみ受け付ける設定になっています。


Route::get('/user', [UserController::class, 'index'])->name('user');

上記のルーティングを変更します。


Route::resource('/user', UserController::class);

名前付きルーティングを利用していますが上記の設定では自動でルーティングに名前がつけられます。名前はphp artisan route:listで確認することができます。user.index, user.storeといったものが名前です。

ルーティングテーブルの確認
ルーティングテーブルの確認

名前付きルートを利用してリンクを設定しているのでNavBar.jsxファイルを更新する必要があります。route関数の引数をuserからuser.indexに更新しています。


<Link
    href={route("user.index")}
    className={url === "/user" ? "active" : ""}
>
    User
</Link>

先ほど作成したCreate.vueファイルに以下を記述します。


import Layout from "../../Layouts/Layout";
import { Head } from "@inertiajs/inertia-react";

const Create = () => {
    return (
        <>
            <Head title="ユーザの登録"></Head>
            <h1>ユーザの登録</h1>
        </>
    );
};

Create.layout = (page) => <Layout children={page} />;

export default Create;

作成したCreate.jsxファイルが表示されるようにUserController.phpファイルにcreateメソッドを追加します。render関数の引数にCreate.jsxファイルを指定します。


public function create(){
    return Inertia::render('User/Create');
}

ブラウザから/user/createにアクセスするとユーザ登録ページが表示されます。

ユーザの登録ページ
ユーザの登録ページ

ユーザの一覧ページ(User¥Index.jsx)からアクセスできるようにリンクを追加します。リンクの設定にはLinkコンポーネントを利用しますがデフォルトではaタグとして設定されますがas属性を利用するとことで他の要素として設定することができます。下記ではボタン要素として設定しています。


<h1>ユーザ一覧</h1>
<div>
    <Link href={route("user.create")} as="button" type="button">
        ユーザ登録
    </Link>
</div>

Linkコンポーネントがボタン要素として表示されていることが確認できます。ボタンをクリックするとユーザ登録画面に移動します。

ユーザ登録のボタンを追加
ユーザ登録のボタンを追加

入力フォームの追加

入力したデータを保存するためuseState Hookを利用します。またPOSTリクエストにはimportしたInertiaを利用しています。


import { Inertia } from "@inertiajs/inertia";
import { useState } from "react";
import Layout from "../../Layouts/Layout";
import { Head } from "@inertiajs/inertia-react";

const Create = () => {
    const [values, setValues] = useState({
        name: "",
        email: "",
        password: "",
    });

    function handleChange(e) {
        const key = e.target.id;
        const value = e.target.value;
        setValues((values) => ({
            ...values,
            [key]: value,
        }));
    }

    function handleSubmit(e) {
        e.preventDefault();
        Inertia.post("/user", values);
    }

    return (
        <>
            <Head title="ユーザの登録"></Head>
            <h1>ユーザの登録</h1>
            <div>
                <form onSubmit={handleSubmit}>
                    <div>
                        <label htmlFor="name">名前:</label>
                        <input
                            id="name"
                            value={values.name}
                            onChange={handleChange}
                        />
                    </div>
                    <div>
                        <label htmlFor="email">メールアドレス:</label>
                        <input
                            id="email"
                            value={values.email}
                            onChange={handleChange}
                        />
                    </div>

                    <div>
                        <label htmlFor="password">パスワード:</label>
                        <input
                            id="password"
                            value={values.password}
                            onChange={handleChange}
                            type="password"
                        />
                    </div>
                    <button type="submit">Submit</button>
                </form>
            </div>
        </>
    );
};

Create.layout = (page) => <Layout children={page} />;

export default Create;

ブラウザで確認すると名前、メールアドレス、パスワードの入力項目を持つフォームが表示されます。

ユーザ登録用の入力フォームの表示
ユーザ登録用の入力フォームの表示

登録ボタンをクリックすると/userにPOSTリクエストが送信されますが送信先が存在しないためエラーメッセージが表示されます。

エラーメッセージの表示
エラーメッセージの表示

UserControllerにstoreメソッドが存在しないためにエラーメッセージが表示されているのでstoreメソッドを追加します。


public function store()
{
    //ユーザ登録処理
    return redirect()->route('user.index');
}

redirect関数を利用することでユーザ登録処理の後にユーザの一覧ページにリダイレクトされます。通常のBladeファイルを利用したLaravelのリダイレクトのようにリダイレクトされる際にページ全体が再読み込みされることはなく更新が必要な箇所のみ更新されます。

axiosなどを利用してPOSTリクエストを送信した場合は処理が完了してJSONデータでユーザ情報が戻された後にクライアント側でリダイレクトの処理を行いますがInertiaの場合はサーバ側でリダイレクトの処理を行うことができます。

ユーザ登録の処理を追加します。


public function store()
{
      User::create(request()->all());

    return redirect()->route('user.index');
}

storeメソッドにユーザ登録の処理を追加したので入力フォームにユーザ情報を入力して”Submit”ボタンをクリックします。

ユーザの登録
ユーザの登録

ユーザの登録が完了するとユーザ一覧の画面にリダイレクトされ登録されたユーザが表示されていることを確認することができます。

登録したユーザの確認
登録したユーザの確認

バリデーションの設定

ユーザ登録時にバリデーションの設定を行っていたなかったのでバリデーションの設定を追加します。


public function store(Request $request)
{

    $validated = $request->validate([
            'name' => ['required', 'max:50'],
            'password' => ['required', 'max:50'],
            'email' => ['required', 'max:50', 'email'],
        ]
    );

    User::create($validated);

    return redirect()->route('user.index');
}

バリデーションを設定後ユーザ登録画面のフォームに何も入力せずに”登録”ボタンをクリックしてください。画面上には何も変化がないはずです。

デベロッパーツールのネットワークタブを開いて再度”登録”ボタンをクリックしてください。ヘッダーを確認するとステータスコード200を確認できます。

ステータスコードの確認
ステータスコードの確認

プレビューを確認するとpropsのerrorオブジェクトの中にバリデーションエラーのメッセージが保存されていることが確認できます。

ネットワークタブのプレビューを確認
ネットワークタブのプレビューを確認

さらにReact Developer Toolsを確認します。pageオブジェクトのpropsのerrorsオブジェクトにメッセージが保存されていることがわかります。

React Developer Toolsによるerrorsの確認
React Developer Toolsによるerrorsの確認

エラーメッセージが保存されていることが確認できたのでページコンポーネント上で表示する方法を確認します。

エラーメッセージはpropsオブジェクトに含まれていることがわかったのでusePage関数を利用してpropsから取得して表示します。

設定後に入力フォームに何も入力せず”登録”ボタンをクリックするとバリデーションエラーが表示されます。


import { Inertia } from "@inertiajs/inertia";
import { useState } from "react";
import Layout from "../../Layouts/Layout";
import { Head, usePage } from "@inertiajs/inertia-react";

const Create = () => {
    const { errors } = usePage().props;
    const [values, setValues] = useState({
        name: "",
        email: "",
        password: "",
    });

    function handleChange(e) {
        const key = e.target.id;
        const value = e.target.value;
        setValues((values) => ({
            ...values,
            [key]: value,
        }));
    }

    function handleSubmit(e) {
        e.preventDefault();
        Inertia.post("/user", values);
    }

    return (
        <>
            <Head title="ユーザの登録"></Head>
            <h1>ユーザの登録</h1>
            <div>
                <form onSubmit={handleSubmit}>
                    <div>
                        <label htmlFor="name">名前:</label>
                        <input
                            id="name"
                            value={values.name}
                            onChange={handleChange}
                        />
                        {errors.name && <div>{errors.name}</div>}
                    </div>

                    <div>
                        <label htmlFor="email">メールアドレス:</label>
                        <input
                            id="email"
                            value={values.email}
                            onChange={handleChange}
                        />
                        {errors.email && <div>{errors.email}</div>}
                    </div>

                    <div>
                        <label htmlFor="password">パスワード:</label>
                        <input
                            id="password"
                            value={values.password}
                            onChange={handleChange}
                            type="password"
                        />
                        {errors.password && <div>{errors.password}</div>}
                    </div>
                    <button type="submit">Submit</button>
                </form>
            </div>
        </>
    );
};

Create.layout = (page) => <Layout children={page} />;

export default Create;
バリデーションエラーの表示
バリデーションエラーの表示

userPage関数を利用してerrorオブジェクトに保存されているメッセージにアクセスを行いましたがuserPage関数を利用せずpropsを直接受け取り表示することもできます。


const Create = ({ errors }) => {
// const { errors } = usePage().props;
//略

バリデーションが発生するとリダイレクトが自動で行われて入力フォームの画面が再度表示されますがリダイレクトされる前に入力した値は保存されているので入力した値をinput要素に再入力するといった処理は必要ありません。

Error bagsの設定

Inertiaを利用してPOSTリクエストを送信する際にError Bagsの設定を行うことができます。


function handleSubmit(e) {
    e.preventDefault();
    // Inertia.post("/user", values);
    Inertia.post("/user", values, { errorBag: "createUser" });
}

errorBagで”createUser”を設定すると通常はprops.errorsオブジェクトに保存されるバリデーションのエラーメッセージがprops.errors.createUserオブジェクトに保存されます。

errorBagが活躍する場面は1つのコンポーネント上に複数のフォームが存在する場合です。どちらのフォームにもnameという入力項目がある場合はnameのバリデーションのエラーメッセージがpage.props.errors.nameに保存されるためどちらのフォームでのエラーが区別つきません。errorBagを利用することでnameのエラーはerrorBagで指定したオブジェクトに保存されるのでどちらのフォームのPOSTリクエストでのエラーか区別することが可能になります。

FormヘルパーのuseFormの利用

先ほど確認した通りInertiaを利用することでPOSTリクエストの送信もバリデーションエラーも処理することができました。Inertia.jsではさらにフォーム処理が効率よく行えるようにForm helperのuseFormを利用することができます。内部ではInertiaを利用しています。

useFormを利用して先ほどのCreate.jsxのコードを書き換えます。useForm Hookの引数のオブジェクトには送信データの初期値を設定します。useState HookはuseFormの中に含まれているため削除することができます。useFormから戻されるオブジェクトにはPOSTリクエストに利用できるpost関数、エラーはerrorsオブジェクトに含まれています。


import Layout from "../../Layouts/Layout";
import { Head, useForm } from "@inertiajs/inertia-react";

const Create = () => {
    const { data, setData, post, errors } = useForm({
        name: "",
        email: "",
        password: "",
    });

    function onSubmit(e) {
        e.preventDefault();
        post("/user");
    }

    return (
        <>
            <Head title="ユーザの登録"></Head>
            <h1>ユーザの登録</h1>
            <div>
                <form onSubmit={onSubmit}>
                    <div>
                        <label htmlFor="name">名前:</label>
                        <input
                            id="name"
                            value={data.name}
                            onChange={(e) => setData("name", e.target.value)}
                        />
                        {errors.name && <div>{errors.name}</div>}
                    </div>

                    <div>
                        <label htmlFor="email">メールアドレス:</label>
                        <input
                            id="email"
                            value={data.email}
                            onChange={(e) => setData("email", e.target.value)}
                        />
                        {errors.email && <div>{errors.email}</div>}
                    </div>

                    <div>
                        <label htmlFor="password">パスワード:</label>
                        <input
                            id="password"
                            value={data.password}
                            onChange={(e) =>
                                setData("password", e.target.value)
                            }
                            type="password"
                        />
                        {errors.password && <div>{errors.password}</div>}
                    </div>
                    <button type="submit">Submit</button>
                </form>
            </div>
        </>
    );
};

Create.layout = (page) => <Layout children={page} />;

export default Create;

userFormはForm helperということで他にもさまざまな関数とプロパティを持っています。useForm関数から戻されるreset関数を利用することでユーザ登録に失敗した場合には入力値をすべてクリアにすることもできます。onErrorイベントはPOSTリクエストに失敗した場合に実行される関数です。onErrorの他にはonStart, onFinishなどのイベントがあります。


const Create = () => {
    const { data, setData, post, reset, errors } = useForm({
        name: "",
        email: "",
        password: "",
    });

    function onSubmit(e) {
        e.preventDefault();
        post("/user", {
            onError: () => reset(),
        });
    }
    //略

バリデーションに失敗してリダイレクトされると入力した値がすべてリセットされます。

POSTリクエストの処理が実行中かどうかを識別するためのprocessingプロパティもあります。processingプロパティを利用することでPOSTリクエスト処理中はボタンをdisabledにすることができます。disabledにすることでボタンを一度押した後に謝って複数回ボタンをクリックすることを防ぐことができます。


const Create = () => {
    const { data, setData, post, reset, errors, processing } = useForm({
        name: "",
        email: "",
        password: "",
    });
//略
<button type="submit" disabled={processing}>
    Submit
</button>

他にForm HelperのuseFromで戻されるオブジェクトがどのような関数やプロパティを持っているかはInertiaのドキュメントで確認することができます。node_modulesの@inertia-react¥src下に保存されているソースコードのuseForm.jsを見ることでも確認することができます。

ファイルのアップロード方法

Form HelperのuseFormを利用したファイルのアップロード方法について確認を行います。usersテーブルにアップロードしたファイルのパスを保存する列の追加を行います。php artisan migrationコマンドを利用してavatar_file_path列を追加するためのマイグレーションファイルを作成します。


 % php artisan make:migration add_avatar_to_users_table --table=users

コマンド実行後に作成されるマイグレーションファイルにavatar_file_pathを追加します。


Schema::table('users', function (Blueprint $table) {
    $table->string('avatar_file_path')->nullable();
});

マイグレーションファイルの更新が完了したらphp artisan migrateコマンドを実行し列の追加を行います。


 % php artisan migrate
Migrating: 2022_04_13_111906_add_avatar_to_users_table
Migrated:  2022_04_13_111906_add_avatar_to_users_table (4.28ms)

User.phpファイルで追加したavatar_file_path列にデータが保存できるように$fillable変数の配列に追加します。


protected $fillable = [
    'name',
    'email',
    'password',
    'avatar_file_path'
];

列の追加が完了したらCreate.jsxファイルにファイルのアップロード用のinput要素を追加します。ファイルを扱うのでtype属性をfile、属性nameにavatarを設定します。


<div>
    <label htmlFor="avatar">画像:</label>
    <input
        type="file"
        onChange={(e) =>
            setData("avatar", e.target.files[0])
        }
    />
    {errors.avatar && <div>{errors.avatar}</div>}
</div>

useFromの引数のオブジェクトにavatarを追加して初期値を””とします。


const { data, setData, post, errors, processing } = useForm({
    name: "",
    email: "",
    password: "",
    avatar: "",
});

ユーザの登録画面を表示するとファイル選択が表示されます。

ファイル選択が追加されたフォーム
ファイル選択が追加されたフォーム

ファイルを選択して”登録”ボタンをクリックする前に選択したファイルがPOSTリクエストに含まれているか確認するためにUserController.phpファイルのstoreメソッドでdd関数を利用してリクエストの中身を確認します。

storeメソッドで設定が完了したら入力フォームでファイルを選択して”登録”ボタンをクリックしてください。POSTリクエストで送信されてきたファイルをコントローラーで受け取れることが確認できます。

送信されてきた画像の確認
送信されてきた画像の確認

コントローラーで送信したファイルを受け取ることができたら画像の保存方法については通常のLaravelでの設定方法と同じです。

保存したファイルがブラウザから閲覧できるようにシンボクックリンクの設定を行います。


% php artisan storage:link

実行すると/storage/app/public以下に保存したファイルがブラウザ上からアクセス可能になります。

storeメソッドにファイルを保存するための処理を追加します。ファイルを選択してリクエストを送信した場合は/storage/app/publicの下にファイルが保存されブラウザから/storage/ファイル名でアクセスすることができるようになります。


public function store(Request $request)
{

    $validated = $request->validate([
            'name' => ['required', 'max:50'],
            'password' => ['required', 'max:50'],
            'email' => ['required', 'max:50', 'email'],
        ]
    );

    if($request->avatar){
        $file_name = $request->file('avatar')->getClientOriginalName();
        $request->file('avatar')->storeAs('public',$file_name);
        $validated['avatar_file_path'] = '/storage/'.$file_name;
    }

    User::create($validated);

    return redirect()->route('user.index');
}

入力フォームに入力後ファイルを選択して登録ボタンをクリックしてファイルのアップロードを行ってください。リダイレクトされるUser¥Index.vueファイルでアップロードした画像を確認できるように更新を行います。avatar_file_pathに値がある時のみ画像を表示させます。


<ul>
    {props.users.map((user) => (
        <li
            key={user.id}
            style={{ display: "flex", alignItems: "center" }}
        >
            {user.avatar_file_path && (
                <img
                    src={user.avatar_file_path}
                    style={{ width: "30px" }}
                />
            )}
            <span>{user.name}</span>
        </li>
    ))}
</ul>

アップロードした画像がユーザ名の左側に表示されれファイルのアップロードは正常に完了しています。

アップロードした画像の表示
アップロードした画像の表示

Shared dataの設定

アプリケーション全体で共有したいデータがある場合にShared dataを利用することができます。ヘッダー上にログインが完了したユーザ名を表示したい場合や何かの処理後にユーザにメッセージを通知するフラッシュメッセージなどに利用することができます。

現在の設定ではログインの機能を持っていないのでログインユーザの利用方法についての説明は行いませんが別の方法でデータを共有する方法を確認します。データの共有はInertia.jsの初期設定時にインストールしたmiddlewareのHandleInertiaRequests.phpファイルのshareメソッドで行うことができます。

middlewareなのでページの移動などリクエストを行う度にshareメソッドが実行されます。

app.phpファイルに設定されているApplication NameをShared dataとして設定する場合に下記のように行うことができます。


public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'appName' => config('app.name'),
    ]);
}

設定後React Developer Toolsを確認するとpropsにappNameが保存されていることが確認できます。Laravelという値になっていますが.envファイルの環境変数APP_NAMEに設定された値です。

propsの中にappNameを確認
propsの中にappNameを確認

About.jsxページからアクセスする場合usePage関数を利用することができます。


import Layout from "../Layouts/Layout";
import { Head, usePage } from "@inertiajs/inertia-react";

const About = () => {
    const { appName } = usePage().props;
    return (
       //略
            <h1>About | {appName}</h1>
        </>
    );
};

設定後はブラウザ上ではAboutの横にLaravelが表示されます。

NavBarコンポーネントからもアクセスすることができます。ナビゲーションメニューのli要素に追加します。


import { Link } from "@inertiajs/inertia-react";
import { usePage } from "@inertiajs/inertia-react";
import "./NavBar.css";

const NavBar = () => {
    const { url, props } = usePage();
    return (
        <>
            <nav>
                <ul>
                //略
                    <li>{props.appName}</li>
                </ul>
            </nav>
        </>
    );
};

export default NavBar;

usePage関数を利用せずpropsを使うこともできます。Layout.jsxファイルはchildrenの中のpropsからアクセスすることができます。


import NavBar from "../Components/NavBar";
import { Head } from "@inertiajs/inertia-react";

const Layout = ({ children }) => {
    return (
        <>
           //略
            <header>
                <h1 style={{ textAlign: "center" }}>
                    {children.props.appName}
                </h1>
                <NavBar />
            </header>
            <main>{children}</main>
        </>
    );
};

export default Layout;

About.jsx, NavBar.jsx, Layout.jsxにShared dataを設定したのでブラウザ上では3つのLaravelが表示されます。

ブラウザ上に表示したShard data
ブラウザ上に表示したShard data

Shared dataはフラッシュデータにも利用することができます。フラッシュデータは次のリクエストの画面にメッセージを表示させるためにSessionに保存するデータのことです。Laravelでは何か処理を行なった後にリダイレクト先でユーザに通知させるメッセージとして利用されます。

ここではユーザ登録が完了した後のリダイレクト先のユーザ一覧ページに表示させるメッセージをShared dataを利用して行います。

フラッシュデータの保存はflashメソッドを使って行うことができます。UserController.phpファイルのユーザを作成後に実行しています。


User::create($validated);

$request->session()->flash('message','ユーザの作成が完了しました');

return redirect()->route('user.index');

保存したメッセージはgetメソッドを利用して取得することができます。


$request->session()->get('message')

Shared dataにフラッシュデータを保存するためHandleInertiaRequests.phpファイルのshareメソッドに処理を追加します。


public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'appName' => config('app.name'),
        'flash' => [
            'message' => fn () => $request->session()->get('message')
        ],
    ]);
}

Shared dataに保存されたデータの取得方法はappNameの場合と同じです。usePage関数から取得することができます。User¥Index.jsxファイルではflashオブジェクトのmessageにメッセーが保存されている場合のみ表示できるように設定を行います。


import { Link, usePage } from "@inertiajs/inertia-react";
import Layout from "../../Layouts/Layout";

const Index = (props) => {
    const { flash } = usePage().props;
    return (
        <>
            <h1>ユーザ一覧</h1>
            {flash.message && (
                <div style={{ color: "red", fontWeight: "bold" }}>
                    {flash.message}
                </div>
            )}
//略
        </>
    );
};

Index.layout = (page) => <Layout children={page} />;

export default Index;

ユーザ登録が完了した場合のみメッセージが表示されます。

Shared dataのflushのメッセージを表示
Shared dataのflushのメッセージを表示

Inertia.jsに記載されているすべての機能の動作確認を行なったわけではありませんがInertia.jsを使いこなう上で必要な機能を確認することができました。

LaravelでReactを利用したいという人がいたらぜひLaravel + Inertia.jsでの環境構築にチャレンジしてください。