環境・使用ライブラリ
PHP 8.2Laravel 10laravel breeze 1.2React Router 6.10React Query 4.28Laravelのインストール
$ composer create-project --prefer-dist laravel/laravel laravel-react-spa$ cd laravel-react-spaLaravelの初期設定
configのapp.phpを開き、言語設定をします。
return [ // ... 'timezone' => 'Asia/Tokyo', 'locale' => 'ja', 'faker_locale' => 'ja_JP',次はデータベースの設定です。
デフォルトではMySQLですが、今回はSQLiteを使用します。
.envを開いて、DB_CONNECTION=sqliteを追記して既存のMySQLの設定部分は削除します。
DB_CONNECTION=sqlite
#DB_CONNECTION=mysql#DB_HOST=127.0.0.1#DB_PORT=3306#DB_DATABASE=homestead#DB_USERNAME=homestead#DB_PASSWORD=secrettouchコマンドでsqliteファイルを作成。
$ touch database/database.sqliteLaravel Breezeの設定
Laravel Breezeはログイン、ユーザー登録、パスワードリセットはスキャフォルドで作成します。
実はBreezeだけでフロントの画面も作ってくれるのですが、Laravel独自になりすぎてあまり汎用的ではないので今回はバックエンドの機能だけを利用します。
コンポーザーでインストールしましょう。
$ composer require laravel/breeze --devインストールコマンドを実行します。
apiオプションを指定してPHPファイルだけ生成するようにしましょう。
$ php artisan breeze:install apiこれでいろいろファイルが書き出されます。
「routes/auth.php」開いてください。
ログインが「/login」、ログアウトが「/logout」に割り当ててありますが、このパスはフロントで使用するので、バックエンドでは「/api/login」「/api/logout」になるように編集します。
Route::prefix('api')->name('api.')->group(function () { Route::post('login', [AuthenticatedSessionController::class, 'store']) ->middleware('guest') ->name('login');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) ->middleware('auth') ->name('logout');});ルートの確認をしてみます。
$ php artisan route:listlogin,logoutとかが追加されているのがわかります。
GET|HEAD / ...............................................................................................................POST _ignition/execute-solution ........ ignition.executeSolution › Spatie\LaravelIgnition › ExecuteSolutionControllerGET|HEAD _ignition/health-check .................... ignition.healthCheck › Spatie\LaravelIgnition › HealthCheckControllerPOST _ignition/update-config ................. ignition.updateConfig › Spatie\LaravelIgnition › UpdateConfigControllerGET|HEAD api/user ........................................................................................................POST api/email/verification-notification .......... verification.send › Auth\EmailVerificationNotificationController@storePOST api/forgot-password ......................................... password.email › Auth\PasswordResetLinkController@storePOST api/login ......................................................... login › Auth\AuthenticatedSessionController@storePOST api/logout ..................................................... logout › Auth\AuthenticatedSessionController@destroyPOST api/register ......................................................... register › Auth\RegisteredUserController@storePOST api/reset-password ................................................ password.store › Auth\NewPasswordController@storeGET|HEAD api/verify-email/{id}/{hash} ....................................... verification.verify › Auth\VerifyEmailControllerGET|HEAD sanctum/csrf-cookie ........................... sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@showPHPファイルを生成すると同時にpackage.jsonとかvite.config.jsファイルが削除されてしまいます。
これはフロントの開発に使うので戻しておきましょう。
.envのSANCTUM_STATEFUL_DOMAINSも設定します。
今回はビルドインサーバーの初期設定のまま使うので「localhost:8000」です。
SANCTUM_STATEFUL_DOMAINS=localhost:8000データベースの構築
シーダーファイルにユーザーデータを投入するfactoryがコメントアウトしてありますので外して有効にします。
class DatabaseSeeder extends Seeder{ public function run(): void { \App\Models\User::factory(10)->create();
\App\Models\User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); }}migrateコマンドを実行してテーブルの作成&データを投入します。
$ php artisan migrate --seedフロント開発環境の構築
Viteの設定をしてReactの開発環境を構築します。
package.jsonファイルがあることを確認してyarnコマンドを実行してnodeモジュールをインストールします。
$ yarn必要なモジュールを追加します。
$ yarn add -D react react-dom @types/react @types/react-dom @vitejs/plugin-react @vitejs/plugin-react-refresh react-router-dom @tanstack/react-query現在(2023/3/26)インストールするとバージョンはこのようになります。
{ "private": true, "scripts": { "dev": "vite", "build": "vite build" }, "devDependencies": { "@tanstack/react-query": "^4.28.0", "@types/react": "^18.0.29", "@types/react-dom": "^18.0.11", "@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react-refresh": "^1.3.6", "axios": "^1.1.2", "laravel-vite-plugin": "^0.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.9.0", "vite": "^4.0.0" }}TypeScriptとReactが使えるようにViteの設定をします。
import { defineConfig } from 'vite';import laravel from 'laravel-vite-plugin';import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/ts/app.tsx'], refresh: true, }), react() ], resolve: { alias: { '@': '/resources/ts', } }});「resources/ts/index.tsx」を読み込む設定にしたので仮で作成しておきます。
import React from 'react'import { createRoot } from 'react-dom/client'
const root = createRoot( document.getElementById('app') as HTMLElement)
root.render( <React.StrictMode> <h1>Hello React !</h1> </React.StrictMode>)ベースとなるbladeファイルを作成します。
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Laravel React SPA</title></head><body><div id="app"></div>@viteReactRefresh@vite(['resources/css/app.css', 'resources/ts/index.tsx'])</body></html>SPAの場合は設定したエンドポイント以外はすべてこのbladeファイルが表示されるようにします。
Route::any('{all}', fn () => view('app')) ->where(['all' => '^(?!api/*).*']);ここまできたら表示の確認をしてみましょう。
今回はビルドインサーバーで簡易的に確認します。
$ yarn run dev$ php artisan serv「http://localhost:8000」にアクセスして「Hello React !」と表示されるか確認できたら設定は完了です。
設定方法は下記のページも参考にしてください。
Laravelに新しく導入されたフロントビルドツール「Vite」でTypeScript+React開発環境を構築する方法
React Routerの設定
次はReact Routerでページのベースを作っていきます。
最初にページコンポーネントを作ります。
作成するのはログインページとログインしないと見れないページのダッシュボードの2ページです。
import React from 'react'
const LoginPage: React.FC = () => { return ( <div> <h1>ログイン</h1> </div> )}
export default LoginPageimport React from 'react'
const DashboardPage: React.FC = () => { return ( <div> <h1>ダッシュボード</h1> </div> )}
export default DashboardPageこのページを表示する為のルーターファイルを作成
import { createBrowserRouter } from 'react-router-dom'import DashboardPage from '@/pages/Dashboard'import LoginPage from '@/pages/Login'
export const router = createBrowserRouter([ { path: 'login', element: <LoginPage /> }, { path: '/', element: <DashboardPage /> }])作成したルーターをApp.tsxで設定します。
import React from 'react'import { RouterProvider } from 'react-router-dom'import { router } from './routes'
const App: React.FC = () => { return ( <RouterProvider router={router} /> )}
export default Appindex.tsxで作成したApp.tsxを表示するようにします。
import React from 'react'import { createRoot } from 'react-dom/client'import App from '@/App'
const root = createRoot( document.getElementById('app') as HTMLElement)
root.render( <React.StrictMode> <App /> </React.StrictMode>)ここまでできたらブラウザで「/」と「/login」にアクセスして作成したページが表示されることを確認してください。
React Query設定
React Queryを使えるようにします。
新しくQueryClientを初期化するファイルを作成します。
基本的な設定はここで行います。
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false }, mutations: { retry: false } }})App.tsxで作成したqueryClientをインポートしてQueryClientProviderの設定をします。
import React from 'react'import { RouterProvider } from 'react-router-dom'import { router } from './routes'import { QueryClientProvider } from '@tanstack/react-query'import { queryClient } from '@/queryClient'
const App: React.FC = () => { return ( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider> )}
export default AppこれでReact Queryを使用する準備ができました。
ログイン中の判定
初期のLaravelの状態だと「api/user」にアクセスするとログイン中のユーザー情報が取得できるので、この情報が取れるかでどうかでログイン中かを判定します。
最初の取得する情報のtypeファイルを作成します。
export type User = { id: number name: string email: string}AxiosでAPIと通信する処理です。
import axios from 'axios'import type { User } from '@/types/User'
export const getUser = async () => { const { data } = await axios.get<User>('/api/user') return data}AxiosのReactQueryに設定します。
import * as api from '@/api/authAPI'import { queryClient } from '@/queryClient'
const authUserQuery = () => ({ queryKey: ['user'], queryFn: api.getUser})
export const useAuthUser = async () => { const query = authUserQuery()
return queryClient.getQueryData(query.queryKey) ?? (await queryClient.fetchQuery(query).catch(() => undefined))}ReactRouterのLoader機能を使用してページを表示する前にAPIからデータを取得できるかできないかでページのリダイレクトを行います。
import { createBrowserRouter, redirect } from 'react-router-dom'import { useAuthUser } from '@/hooks/useAuth'import DashboardPage from '@/pages/Dashboard'import LoginPage from '@/pages/Login'
/** * ログイン済みのみアクセス可能 */const guardLoader = async () => { const user = await useAuthUser() return user ? true : redirect('/login')}
/** * ログインしていない場合のみアクセス可能 */const guestLoader = async () => { const user = await useAuthUser() return user ? redirect('/') : true}
export const router = createBrowserRouter([ { path: 'login', element: <LoginPage />, loader: guestLoader }, { path: '/', element: <DashboardPage />, loader: guardLoader }])「/」にアクセスすると「/login」にリダイレクトされることを確認してください。
ログインとログアウト
APIに接続するところから作ります。
export const login = async ({ email, password }: { email: string, password: string}) => { const { data } = await axios.post<User>( '/api/login', { email, password } ) return data}
export const logout = async () => { const { data } = await axios.post<User>('/api/logout') return data}hooksにも追加します。
ログインできなかったときの処理も必要になりますが、今回は省略します。
export const useLogin = () => { const queryClient = useQueryClient()
return useMutation({ mutationFn: api.login, onError: (error: AxiosError) => { console.log(error) }, onSuccess: (data) => { console.log(data) queryClient.invalidateQueries(['auth']) window.location.href = '/' } })}
export const useLogout = () => { const queryClient = useQueryClient()
return useMutation({ mutationFn: api.logout, onError: (error: AxiosError) => { console.log(error) }, onSuccess: (data) => { console.log(data) queryClient.invalidateQueries(['auth']) window.location.href = '/login' } })}ログイン画面を修正して、作成したuseLoginを実行できるようにします。
import React, { useState } from 'react'import { useLogin } from '@/hooks/useAuth'
const LoginPage: React.FC = () => { const login = useLogin()
const [email, setEmail] = useState('') const [password, setPassword] = useState('')
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault()
login.mutate({ email: email, password: password, }) }
return ( <div> <h1>ログイン</h1> <form onSubmit={onSubmit}> <fieldset> <div> <label htmlFor="email">メールアドレス:</label> <input type="email" id="email" onChange={e => setEmail(e.target.value)} defaultValue={email} /> </div> <div> <label htmlFor="password">パスワード:</label> <input type="password" id="password" onChange={e => setPassword(e.target.value)} defaultValue={password} /> </div> <button type="submit">送信</button> </fieldset> </form> </div> )}
export default LoginPageダッシュボードではログアウトボタンを追加して、useLogoutを実行できるようにします。
import React from 'react'import { useLogout } from '@/hooks/useAuth'
const DashboardPage: React.FC = () => { const logout = useLogout()
const onLogout = () => { logout.mutate() }
return ( <div> <h1>ダッシュボード</h1> <button type="button" onClick={onLogout}>ログアウト</button> </div> )}
export default DashboardPageこれで完成です。
ログイン画面からログインできることを確認してください。
初期データのメールアドレスは「test@example.com」、パスワードは「password」です。
ログインしている状態ではダッシュボードが見れて、ログイン画面が見れない。
ログインしていない状態の場合、逆にログイン画面が見れて、ダッシュボードが見れないという状態になっていると思います。

