環境・使用ライブラリ
PHP 8.2 Laravel 10 laravel breeze 1.2 React Router 6.10 React Query 4.28
Laravelのインストール
$ composer create-project --prefer-dist laravel/laravel laravel-react-spa $ cd laravel-react-spa
Laravelの初期設定
configのapp.phpを開き、言語設定をします。
config/app.php
return [ // ... 'timezone' => 'Asia/Tokyo', 'locale' => 'ja', 'faker_locale' => 'ja_JP',
次はデータベースの設定です。
デフォルトではMySQLですが、今回はSQLiteを使用します。
.env
を開いて、DB_CONNECTION=sqlite
を追記して既存のMySQLの設定部分は削除します。
.env
DB_CONNECTION=sqlite #DB_CONNECTION=mysql #DB_HOST=127.0.0.1 #DB_PORT=3306 #DB_DATABASE=homestead #DB_USERNAME=homestead #DB_PASSWORD=secret
touchコマンドでsqliteファイルを作成。
$ touch database/database.sqlite
Laravel 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」になるように編集します。
routes/auth.php
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:list
login,logoutとかが追加されているのがわかります。
GET|HEAD / ............................................................................................................... POST _ignition/execute-solution ........ ignition.executeSolution › Spatie\LaravelIgnition › ExecuteSolutionController GET|HEAD _ignition/health-check .................... ignition.healthCheck › Spatie\LaravelIgnition › HealthCheckController POST _ignition/update-config ................. ignition.updateConfig › Spatie\LaravelIgnition › UpdateConfigController GET|HEAD api/user ........................................................................................................ POST api/email/verification-notification .......... verification.send › Auth\EmailVerificationNotificationController@store POST api/forgot-password ......................................... password.email › Auth\PasswordResetLinkController@store POST api/login ......................................................... login › Auth\AuthenticatedSessionController@store POST api/logout ..................................................... logout › Auth\AuthenticatedSessionController@destroy POST api/register ......................................................... register › Auth\RegisteredUserController@store POST api/reset-password ................................................ password.store › Auth\NewPasswordController@store GET|HEAD api/verify-email/{id}/{hash} ....................................... verification.verify › Auth\VerifyEmailController GET|HEAD sanctum/csrf-cookie ........................... sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@show
PHPファイルを生成すると同時にpackage.jsonとかvite.config.jsファイルが削除されてしまいます。
これはフロントの開発に使うので戻しておきましょう。
.envのSANCTUM_STATEFUL_DOMAINSも設定します。
今回はビルドインサーバーの初期設定のまま使うので「localhost:8000」です。
.env
SANCTUM_STATEFUL_DOMAINS=localhost:8000
データベースの構築
シーダーファイルにユーザーデータを投入するfactoryがコメントアウトしてありますので外して有効にします。
database/seeders/DatabaseSeeder.php
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)インストールするとバージョンはこのようになります。
package.json
{ "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の設定をします。
vite.config.js
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」を読み込む設定にしたので仮で作成しておきます。
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ファイルを作成します。
resources/views/app.blade.php
<!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ファイルが表示されるようにします。
routes/web.php
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ページです。
resources/ts/pages/Login.tsx
import React from 'react' const LoginPage: React.FC = () => { return ( <div> <h1>ログイン</h1> </div> ) } export default LoginPage
resources/ts/pages/Dashboard.tsx
import React from 'react' const DashboardPage: React.FC = () => { return ( <div> <h1>ダッシュボード</h1> </div> ) } export default DashboardPage
このページを表示する為のルーターファイルを作成
resources/ts/routes.tsx
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で設定します。
resources/ts/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 App
index.tsxで作成したApp.tsxを表示するようにします。
resources/ts/index.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を初期化するファイルを作成します。
基本的な設定はここで行います。
resources/ts/queryClient.ts
import { QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false }, mutations: { retry: false } } })
App.tsxで作成したqueryClientをインポートしてQueryClientProviderの設定をします。
resources/ts/App.tsx
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ファイルを作成します。
resources/ts/types/User.ts
export type User = { id: number name: string email: string }
AxiosでAPIと通信する処理です。
resources/ts/api/authAPI.ts
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に設定します。
resources/ts/hooks/useAuth.ts
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からデータを取得できるかできないかでページのリダイレクトを行います。
resources/ts/hooks/useAuth.ts
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に接続するところから作ります。
resources/ts/api/authAPI.ts
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にも追加します。
ログインできなかったときの処理も必要になりますが、今回は省略します。
resources/ts/hooks/useAuth.ts
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を実行できるようにします。
resources/ts/pages/Login.tsx
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を実行できるようにします。
resources/ts/pages/Dashboard.tsx
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」です。
ログインしている状態ではダッシュボードが見れて、ログイン画面が見れない。
ログインしていない状態の場合、逆にログイン画面が見れて、ダッシュボードが見れないという状態になっていると思います。