LoginSignup
0
0

laravel Breeze API + React SPA

Last updated at Posted at 2024-04-21

参考サイト

ディレクトリ構成

.
└── laravel-Breeze-react-spa
    ├── BACK_END (laravel)
    └── FRONT_END (React)

Laravel10のインストール

$
composer create-project laravel/laravel:^10.0 BACK_END
$
cd BACK_END

sqliteの作成

$
touch database/database.sqlite
.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

テーブルとユーザー(ダミー)の作成

database\seeders\DatabaseSeeder.php
    public function run(): void
    {
        // \App\Models\User::factory(10)->create();
// passwordはデフォルトのpasswordになる
        \App\Models\User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
$
php artisan migrate --seed

Laravel Breeze APIのインストール

  • ユーザーの認証情報をサーバー側で管理する、セッションベースの認証方法
    • トークンベースの認証は、各リクエストに認証トークンを含めることで、ユーザーを認証する方法
  • ユーザー登録、ログイン、ログアウトなどの基本的な認証プロセスがAPI経由で行えるようになります
  • 必要な認証関連のルーティング、コントローラー、ミドルウェアがセットアップされます
  • フロントエンドのビューファイルは不要なので、それらはインストールされません

Laravel Breezeのインストール

  1. Composerを使用してBreezeパッケージをプロジェクトに追加
  2. その後、php artisan breeze:install api コマンドを実行して、API用の認証機能をセットアップ
$
composer require laravel/breeze --dev

👇を実行するとフロントエンドのためのファイル package.json とか vite.config.js削除される。

$
 php artisan breeze:install api
routes\web.php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('index');
});
// ↓web.php に追加される。
require __DIR__.'/auth.php';

routes\auth.phpが作成される

routes\auth.php
<?php

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;

// ユーザー登録のためのルート
Route::post('/register', [RegisteredUserController::class, 'store'])
                ->middleware('guest') // ゲストのみアクセス可能
                ->name('register'); // ルート名は'register'
                // 認証済みでアクセスした場合、302でリダイレクトされる。

// ログインのためのルート
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
                ->middleware('guest') 
                ->name('login'); 

// ユーザーがパスワードを忘れたときにパスワードリセットリンクをリクエストするためのものです。
// このルートにメールアドレスをpostしてpostしたメールアドレスにパスワードリセットリンクが送信されます。
Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])
                ->middleware('guest') 
                ->name('password.email'); 

// ユーザーがパスワードリセットリンクを受け取った後、新しいパスワードを設定するために使用されます。
// パスワードリセットリンクからtokenを取得して、emailとnewPasswordをこのルートにポストすると
// passwordが新しくDBにセットされるルート
Route::post('/reset-password', [NewPasswordController::class, 'store'])
                ->middleware('guest') 
                ->name('password.store'); 

// メールアドレスの確認のためのルート
Route::get('/verify-email/{id}/{hash}', VerifyEmailController::class)
                // 認証済み、署名済み、制限付きアクセス
                ->middleware(['auth', 'signed', 'throttle:6,1']) 
                ->name('verification.verify');
                // 署名済み: URLの{id}{hash}とURLの制限時間が検証され、{id}{hash}と時間が無効である場合は403エラーが返されます
                // 制限付きアクセス: 'throttle:6,1' は「1分間に最大6回までとリミットに達した場合の待機時間1分」という制限 
                //  'throttle:10,5' 「1分間に最大10回までとリミットに達した場合の待機時間5分」という制限

// 上のルートのリンクを作成してメールに送るルート
Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
                ->middleware(['auth', 'throttle:6,1']) // 認証済み、制限付きアクセス
                ->name('verification.send'); 

// ログアウトのためのルート
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
                ->middleware('auth') // 認証済みのみアクセス可能
                ->name('logout'); 

不要なルートの削除

routes\api.php
<?php
// コメントアウト
// Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
//     return $request->user();
// });

ルートリストの確認

php artisan route:list
// ホームページを表示するためのルート
GET|HEAD   / 

// laravelのデバックのためのルート
POST       _ignition/execute-solution 

// laravelのデバックのためのルート
GET|HEAD   _ignition/health-check 

// laravelのデバックのためのルート
POST       _ignition/update-config 

// メールアドレス確認通知を再送するためのルート
POST       email/verification-notification 

// postしたメールアドレスにパスワードリセットリンク付きメールを送信
POST       forgot-password 

// ユーザーログインを処理するためのルート
POST       login 

// ユーザーログアウトを処理するためのルート
POST       logout 

// ユーザー登録を処理するためのルート
POST       register 

// パスワードリセットリンクを受け取った後、新しいパスワードをpostして更新するルート
POST       reset-password 

// CSRFトークンのクッキーを取得するためのルート
GET|HEAD   sanctum/csrf-cookie 

// メールアドレスの確認を処理するためのルート
GET|HEAD   verify-email/{id}/{hash}

認証済みかそうでないかのチェックをするルートの作成

認証済みのユーザーを取得するルートの作成

BACK_END_bk\routes\web.php
//+
Route::get('/is-login', fn() => Auth::check())
    ->name('is-login');

//+
Route::get('/get-user', fn() => Auth::User())
    ->middleware('auth')->name('get-user');

login処理の修正

login成功時、jsonでuser情報を返すように修正する

app\Http\Controllers\Auth\AuthenticatedSessionController.php
//+
use Illuminate\Http\JsonResponse;

// 戻り値の型を JsonResponse に修正
    // public function store(LoginRequest $request): Response
    public function store(LoginRequest $request): JsonResponse
    {
    // 認証処理 失敗すればエラーが飛ぶ
        $request->authenticate();
    // session_id の再取得
        $request->session()->regenerate();

// -
//        return response()->noContent();

//+
    // Auth ファサードを使用して現在認証されているユーザーを取得
        $user = Auth::user();
        
    // ユーザー情報をJSON形式で返す
        return response()->json([
            'id' => $user->id,
            'name' => $user->name,
            'email' => $user->email,
            'is_login' => true
        ]);
    }

認証済みによる302リダイレクト処理をjsonをレスポンスするように修正する

ログイン中にログイン認証を再度求めると、ミドルウェアの['guest']によって302リダイレクトされる。

app\Http\Middleware\RedirectIfAuthenticated.php
<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class RedirectIfAuthenticated
{
    /**
     * @param  \Illuminate\Http\Request  $request  受信したリクエスト
     * @param  \Closure  $next  次に呼び出すミドルウェア
     * @param  string ...$guards  使用する認証ガード(複数指定可能)
     * @return \Symfony\Component\HttpFoundation\Response  返すレスポンス
     */
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
     // ユーザーが認証中かどうか?
            if (Auth::guard($guard)->check()) {
//+
                if ($request->expectsJson()) {
    // リクエストがJSONを期待している場合、認証済みであることを示すJSONレスポンスを返します。
                    return response()->json(['message' => 'You are already authenticated.'], 200);
                }

            // ユーザーが認証済みでJSONを期待していない場合、ホームページにリダイレクトします。
                return redirect(RouteServiceProvider::HOME);
            }
        }

        // ユーザーが認証されていない場合、リクエストを次のミドルウェアに進めます。
        return $next($request);
    }
}

CORS(クロスオリジンリクエスト)のサーバー側の設定

laravelのBreeze API認証ではセッションベースの認証のため、

cookie送信が必須条件

sactum-csrf-cookieにゲットメソッドでアクセスするときも、
register,login,logoutするときも常にcookieを送信する必要がある

かつ、クロスオリジン間での通信のため

サーバー側でもcors設定が必要

BACK_END\config\cors.php
<?php

return [
// CORSポリシーが適用されるパス。'*'はすべてのパスに適用されることを意味します。
    'paths' => ['*'], 
    // 'paths' => ['/sanctum-csrf-cookie','/login'], 

// CORSリクエストで許可されるHTTPメソッド。
    'allowed_methods' => ['*'], 

// 😤 必須
// CORSリクエストを許可するオリジン。
// クロスオリジンの場合 * は 不可
    'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], 

 // 許可されるオリジンのパターンを正規表現で指定。ここではパターンは指定されていません。
    'allowed_origins_patterns' => [],

// リクエストで許可されるHTTPヘッダー。
    'allowed_headers' => ['*'], 

// レスポンスでクライアントに公開されるヘッダー。
// ここでは公開されるヘッダーは指定されていません。
    'exposed_headers' => [], 

// ブラウザがCORSレスポンスをキャッシュする最大時間(秒)。
// `0`はキャッシュしないことを意味します。
    'max_age' => 0, 

// 😤 必須 true
// クロスオリジンリクエストで認証情報(クッキーなど)を送信するかどうか。
//  `true`は認証情報を送信することを許可します。
    'supports_credentials' => true,
];
env
FRONTEND_URL=http://localhost:3000 

laravelの起動 デフォルトでは localhost:8000

$
php artisan serve

Reactのインストール

$ ルートディレクトリに戻って
cd ../
npm create vite@4

Project name: FRONT_END
Package name: react-spa
Select a framework: » React
Select a variant: » JavaScript

  • dom,router,queryのインストール
$
cd FRONT_END; npm install react react-dom react-router-dom @tanstack/react-query

portを3000に修正

FRONT_END\package.json
  "scripts": {
    "dev": "vite --port 3000",

  },

reactの起動

$
npm run dev
FRONT_END\src\main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
// コメントアウト
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Reactのファイル名について
コンポーネントのファイル名やクラス名に関するものでパスカルケース(Pascal Case)
API関数などのユーティリティファイルにはキャメルケース(camelCase)が一般的に使用される。

React createBrowserRouterの設定

createBrowserRouterを使用すると、静的に定義されたルートセットを持つルーターをReactツリーの外部で作成できます。これにより、フェッチとレンダリングの分離が可能になる。

FRONT_END\src\router.jsx
import { NavLink, Outlet, createBrowserRouter } from 'react-router-dom'
import LoginPage from './pages/LoginPage'
import DashboardPage from './pages/DashboardPage'

// path,element,childrenがルートセットになる
export const router = createBrowserRouter([
    {
        path: "/",
        element: <Layout />,
        children: [
            {
                path: 'login',
                element: <LoginPage />
            }, {
                path: '/',
                element: <DashboardPage />
            }
        ]
    }
])

function Layout() {
    return (
        <div>
            <Navbar />
            <Outlet /> {/* childrenのコンポーネントが渡る */}
        </div>
    )
};
function Navbar() {
    return (
        <header> 
{/* NaviLinkは createBrowserRouterの中で設定する必要がある*/}
            <nav>
                <ul>
                    <li>
                        <NavLink to="/">DashBoardPage</NavLink>
                    </li>
                    <li>
                        <NavLink to="/login">LoginPage</NavLink>
                    </li>
                </ul>
            </nav>
        </header>
    );
}

作成したrouter.jsはRouterProviderで提供できる。

FRONT_END\src\App.jsx
import { RouterProvider } from 'react-router-dom'
import { router } from './router'

const App = () => {

  return (
    <>
      {/* <Navibar /> */} {/* これは機能しないし、*/}
      <RouterProvider router={router} /> {/* コンポーネントはこの中にある */}
      {/* web.phpみたいな ルート(path)があって、コントローラー(loader)があって
           特定のビュー(element)を戻す */}
      {/* コンポーネントはブレードファイルみたいな */}
      {/* ただ、レイアウトまでルートで記述するので抵抗がある */}
    </>
  )
}

export default App;
FRONT_END\src\pages\DashboardPage.jsx
const DashboardPage = () => {
    return (
        <div>
            <h1>ダッシュボード</h1>
        </div>
    )
}
 
export default DashboardPage
FRONT_END\src\pages\LoginPage.jsx
const LoginPage = () => {
    return (
        <div>
            <h1>ログイン</h1>
        </div>
    )
}
 
export default LoginPage

ルーティングの確認ができればOK

CSRFとクロスオリジンリクエストによるcookieと信用情報の送信のためのクライアント側からの対策

なぜcookieの送信が必要なのか

laravel Breeze Api は セッションベースの認証をしているため、認証関連がサーバーで使用される場合、
クライアント側からサーバー側に認証情報を含むcookieを送信することで、

サーバー側で認証中のユーザーかそうでないかを判定する

tokenの取得と送信について

  • csrf-tokenの送信が必要な理由

    • laravelの web.php でルートを設定したため、
      webミドルウェアで getメソッド 以外のメソッドで送信する場合
      ヘッダーかボディーに X-CSRF-TOKEN を一緒に送信する必要がある。

      ない場合リダイレクトされる。

      よって

      サーバーからcsrfトークンの値を取得する必要がある

  • cookieからtokenを取り出してヘッダーかボディーでサーバー側に送信する必要がある

    • なぜなら、
      /sanctum/csrf-cookieルートでtokenを取得するとき
      サーバーからは tokenはcookieにセットされて レスポンスされるため
      よって、
      レスポンスされたcookieからtokenを取得して ヘッダーにセットする必要がある

今回はlocalhost:8000と3000のクロスオリジン間のcookieの通信する必要がある

通常、ブラウザのセキュリティポリシーは、異なるオリジン間でのリクエストに対してCookieや認証情報を送信しないように制限している。

しかし、
axiosの場合 withCredentials: trueを設定することで、クロスオリジンリクエストにCookieや認証ヘッダーを含めることができ、また、cookieのtokenを自動でヘッダーにセットしてくれる。

Fetch APIを使用する場合はcredentialsオプションをincludeに設定し、cookieからtokenを取得し、ヘッダーにセットする。

axiosで送信する場合

axiosはxsrfCookieNameプロパティの値を使用してクッキーからCSRFトークンを読み取る
axiosはxsrfHeaderNameプロパティの値を使用してaxios.config.headersにCSRFトークンを自動セットしてくれる

axiosで送信する場合
import axios from 'axios';

const http = axios.create({
  baseURL: 'http://localhost:8000',
  headers: {
    'Content-Type': 'application/json',  // レスポンスはJSONを期待する設定
  },
// クロスオリジン間の通信のためcookieと認証情報を送信する場合は明示する必要がある
  withCredentials: true,
});

// axios1.6.0以上の場合 追加の設定が必要
// axiosからwithCredentials: true,だけでは
// 今回のようなクロスオリジン間の通信ではaxiosからcookieにアクセスできなくなった
const http2 = http.create({
  // 公式には掲載されていない axiosでcookieにアクセスできるようになる
  withXSRFToken: true
})

// axios1.6.0以上の場合 追加の設定が必要
// クロスオリジン間の時 axiosからcookieにアクセスできなくなったため
// 手動でヘッダーにのせる必要がある。
// スタックオーバーフローで掲載されているメジャーな方法
// http.interceptors.request.use(config => {
//   const token = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN=')).split('=')[1];
//   if (token) {
//     config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token);
//   }
//   return config;
// });

export const login = async ({ email, password }) => {
  // CSRFトークンを取得
  await http.get('/sanctum/csrf-cookie');

  // ログインリクエストを送信(token(headers) + cookie + email(body) + password(body))
  const { data } = await http2.post('/login', { email, password });
  console.log(data);
  return data;
};

fetchで送信する場合

fetch送信の場合
// fetch APIを使用してHTTPリクエストを行うための設定
const baseUrl = 'http://localhost:8000';
fetch(`${baseUrl}/sanctum/csrf-cookie`, {
// withCredentials: trueに相当する設定
  credentials: 'include' //cookieを送信
}).then((res) => {
  fetch(`${baseUrl}/login`, {
    method: 'POST', 
    credentials: 'include', // クッキーを含むリクエストを送信する設定
    headers: {
      'Content-Type': 'application/json', // レスポンスはJSONを期待する設定
// CSRFトークンを直接ヘッダーにセットして送信する必要がある。
// csrfTokenをcookieから取得するのは手間
      'X-CSRF-TOKEN': csrfToken, 
// 通常metaタグから取得する
// <meta name="csrf-token" content="{{ csrf_token() }}">
//     'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify({
      email: 'test@example.com', // ユーザーのメールアドレス
      password: 'password' // ユーザーのパスワード
    })
  }).then((response) => {
    return response.json(); // レスポンスのJSONを解析
  }).then((data) => {
    console.log(data); // レスポンスデータをコンソールに出力
  });
});

結論

axosのインストール

$ FRONT_ENDで実行
npm i axios

ログインとログアウト機能の実装

FRONT_END\src\api\authApi.js
import axios from 'axios';

const http = axios.create({
  baseURL: 'http://localhost:8000',
  headers: {
    'Content-Type': 'application/json', // application/json を追加
  },

//信用情報を含むcookieをサーバーに送信してくれる
  withCredentials: true,
});

// axios1.6.0以上の場合 追加の設定が必要
// cookieにアクセスしてconfig.headersにx-xrf-tokenをセットしてくれる
const http2 = http.create({
  // 公式には掲載されていない
  withXSRFToken: true
})

// axios1.6.0以上の場合 axiosからcookieにはアクセスできなくなっている
// スタックオーバーフローでよく掲載されているメジャーな方法
// http.interceptors.request.use(config => {
//   const token = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN=')).split('=')[1];
//   if (token) {
//     config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token);
//   }
//   return config;
// });

export const isLogin = async ()=>{
//Route::get('/is-login', fn() => Auth::check())
// セッションベースの認証を利用するため http で送信する必要がある
  const {data} =  await http.get('is-login');
  return data;
}

export const getUser = async ()=>{
// middleware('auth')があるためセッションベースの認証を利用するため
  const {data} =  await http.get('get-user');
  return data;
}

// ログイン関数をエクスポート
export const login = async ({ email, password }) => {

  // リクエストは、getメソッド
  // token取得のためcookieをおくってリクエストする必要がある
  await http.get('/sanctum/csrf-cookie');

  // ログインリクエストはポストメソッドでリクエストするため、
  // headerにtokenをつけて送信
  const { data } = await http2.post('/login', { email, password });
  console.log(data);
  return data;
};

export const logout = async () => {
  
  await http.get('/sanctum/csrf-cookie')

  const { data } = await http2.post('/logout');
  return data
}

ログイン、ログアウト機能の確認

FRONT_END\src\pages\LoginPage.jsx
import { useState } from 'react'
import * as api from '../api/authApi'

const LoginPage = () => {
    const [loginUser, setLoginUser] = useState({});

    const onLogin = async () => {
        const user = await api.login({
            email: 'test@example.com',
            password: 'password',
        })
        setLoginUser(user);
    }
    const onLogout = async () => {
        try{
            await api.logout()
            setLoginUser({});
        }catch(error){
            setLoginUser(error.response.data);
        }
    }

    return (
        <div>
            <h1>ログインユーザー情報</h1>
            <ul>
                {Object.entries(loginUser).map(([key, value]) => {
                    return <li key={key}>{key}:{value}</li>
                })}
            </ul>
            <button onClick={onLogin}>ログイン</button>
            <button onClick={onLogout}>ログアウト</button>
        </div>
    )
}
export default LoginPage

ログイン、ログアウトしてユーザー情報を取得・削除できればOK

React Query

  • メリット
    • Reaqt Queryのメソッドから api を コールするだけで
      • サーバーからデータを取得する
      • 取得したデータをクライアント側でキャッシュし、再利用できる
      • 状態管理: データの読み込み状態、エラー状態などを管理し、UIの更新を効率的に行える
      • 設定に基づいてデータを自動的に再フェッチし、最新の状態を保つ自動更新機能が行える

React Queryの設定

FRONT_END\src\queryClient.js
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    // クエリのオプション
    queries: {
      // クエリが失敗した場合の再試行回数
      retry: false, // デフォルトでは3回の再試行が行われますが、ここでは無効化しています
      // ウィンドウがフォーカスされたときにクエリを再フェッチするかどうか
      refetchOnWindowFocus: false, // デフォルトではtrueですが、ここでは無効化しています
      // ネットワークがオンラインに戻ったときにクエリを再フェッチするかどうか
      refetchOnReconnect: true, // デフォルトではtrueです
      // ポーリング間隔(ミリ秒)
      refetchInterval: false, // デフォルトでは無効ですが、特定の間隔でデータをポーリングすることができます
      // キャッシュされたデータの新鮮さの期間(ミリ秒)
      staleTime: 0, // デフォルトでは0です。データが古くなったと見なされるまでの時間を指定します
      // クエリがアクティブでなくなった後にキャッシュから削除されるまでの時間(ミリ秒)
      cacheTime: 1000 * 60 * 5, // デフォルトでは5分です
    },
    // ミューテーションのデフォルトオプション
    mutations: {
      // ミューテーションが失敗した場合の再試行回数
      retry: false, // デフォルトでは無効です
    },
  },
});
FRONT_END\src\App.jsx
// React RouterとReact Queryの必要なモジュールをインポートします
import { RouterProvider } from 'react-router-dom';
import { queryClient } from './queryClient';
import { QueryClientProvider } from '@tanstack/react-query';
import { router } from './router';

const App = () => {
    return (
// QueryClientProviderでqueryClientを提供します
// アプリケーションのどこからでもReact Queryの機能を使用できる
        <QueryClientProvider client={queryClient}>
            <RouterProvider router={router} />
        </QueryClientProvider>
    )
}

export default App;
上の設定によってその下の階層では
// これでReact Queryのフックを取得してプロパティやメソッドを利用して
// クエリの発行、キャッシュの取得更新削除ができるようになる。
    const {data:todos} = useQuery({qeryKey,queryFn});
    const {mutate:getTodo} = useMutation();
    const queryClient = useQueryClient();

// 通常React Queryのフックからカスタムフックを作成して使用する
    const {data:todos} = useTodos();

authApi + React Query のカスタムフックの作成

FRONT_END\src\hooks\useAuthApi.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as api from '../api/authApi';

// ログインユーザーのステータスを取得し、キャッシュする
export const useLoginUser = () => {
 // api.isLogin関数でログインしているか true of false
  const queryOptions = {
    // // 初期データを設定する と基本queryFnは実行されなくなる
        // initialData: undefined,
    
    // queryを実行した時刻とかの時刻を設定 新鮮さの判断につかう
    // デフォルトのundefinedは新鮮さはその他で判断する
        // initialDataUpdatedAt: undefined,//Date::now();
    
// データが古くなったと見なされるまでの時間(ミリ秒)
// Infinityを設定すると、データは決して古くならず、自動的に背景更新されることはありません。
// つまり、一度フェッチしたデータは、新しいデータを取得するための再フェッチがトリガーされない限り、
// 常に「新鮮」と見なされます。
        staleTime: Infinity,
    
    // // キャッシュされたデータが削除されるまでの時間(ミリ秒)
        // cacheTime: 5 * 60 * 1000,
    
    // // ウィンドウがフォーカスされたときにデータを再フェッチするかどうか
        refetchOnWindowFocus: false,
    
    // // ネットワークが再接続されたときにデータを再フェッチするかどうか
        // refetchOnReconnect: true,
    
    // // クエリが失敗した場合の再試行回数
        // retry: 3,
    
    // // 再試行の遅延時間(ミリ秒)
        // retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
        
    // // queryFn を実行するかどうか false 実行しない
        // enabled: true ,
      };

// v5からオブジェクト形式で渡す必要がある
    //  return useQuery(['loginUserStatus'], api.getUser, queryOptions);
//optionはスプレッド構文で展開してわたす
  return useQuery({queryKey:['loginUserStatus'],queryFn:api.getUser,...queryOptions}
  );
  // queryKey はキャシュデータを一意に管理する名前
  // queryFnに api をセットする
};

// ログイン処理し、loginUserStatusに fetchしたデータをキャッシュする
export const useLogin = () => {
    const queryClient = useQueryClient();
  
    const mutationOptions = {
      mutationFn: api.login, // api の 登録
  // ミューテーションがエラーになった場合
      onError: (error) => {
        console.error('login error:', error);
      },
  // ミューテーションが成功した場合
      onSuccess: (data) => {
        console.log('login success:', data);

// React Query のキャッシュに保存されているデータが更新されたとき、
// そのデータを参照しているコンポーネントが最新の状態を反映するために再レンダリングされる
        queryClient.setQueryData(['loginUserStatus'], data);
      },

// 失敗時の再試行を行わない
          // retry: false,  

// ミューテーションが成功または失敗した後に実行される処理
          // onSettled: (data, error) => {
          //   if (error) {
          //     console.error('Logout failed:', error);
          //   }
          // },
    };
  
    return useMutation(mutationOptions);
  };

// ログアウト処理を行い、loginUserStatus を更新するカスタムフック
export const useLogout = () => {
  const queryClient = useQueryClient();

  const mutationOptions = {
    mutationFn: api.logout,
    onError: (error) => {
      console.error('Logout error:', error);
    },
    onSuccess: (data) => {
      console.log('Logout success:', data);

      queryClient.setQueryData(['loginUserStatus'], {});
    },
  };

  return useMutation(mutationOptions);
};
上の解説
// useLogin()が return しているのは?
export const useLogin = () => {

// useQueryの実行結果 
  return useQuery({queryKey:['loginUserStatus'], queryFn:api.login,...queryOptions});
};

// useLogout()が
// return しているのは
export const useLogout = () => {
  const mutationOptions = {
    mutationFn: api.logout,
  };
  
// useMutation の実行結果
  return useMutation(mutationOptions);
};
const useQueryの実行結果が戻ってくる = useLogin();
const useMutationの実行結果が戻ってくる = useLogout();

useQueryの実行結果が戻してくる戻り値は?

useQuery() の戻り値
const {
  data: undefined, // フェッチされたデータ <ー これがキャシュされる
  dataUpdatedAt: 0, // データが最後に更新された時刻
  error: null, // エラーオブジェクト
  failureCount: 0, // フェッチ失敗の回数
  isError: false, // エラーが発生したかどうかのブール値
  isFetched: false, // データが一度でもフェッチされたかどうかのブール値
  isFetchedAfterMount: false, // コンポーネントマウント後にデータがフェッチされたかどうかのブール値
  isFetching: false, // データフェッチ中かどうかのブール値
  isPending: true, // ローディング中かどうかのブール値
  isPlaceholderData: false, // プレースホルダーデータを使用しているかどうかのブール値
  isPreviousData: false, // 前回のデータを使用しているかどうかのブール値
  isStale: true, // データが古くなっているかどうかのブール値
  isSuccess: false, // データフェッチが成功したかどうかのブール値
  refetch: Function, // データを再フェッチするための関数
  remove: Function, // クエリをキャッシュから削除するための関数
  status: 'loading' // クエリのステータス ('pending', 'error', 'success')
} = useQuery(queryKey,queryFn);
useMutation() の戻り値
const {
  mutate, // ミューテーション関数 mutationFn
  mutateAsync, // プロミスを返すミューテーション関数
  isPending, // ミューテーションが現在実行中かどうかを示すブール値
  isError, // ミューテーションがエラーで終了したかどうかを示すブール値
  isSuccess, // ミューテーションが成功したかどうかを示すブール値
  error, // ミューテーションが失敗した場合のエラーオブジェクト
  data, // ミューテーションの結果として返されるデータ
  status, // ミューテーションの状態を示す文字列 ('idle', 'pending', 'error', 'success')
  reset, // ミューテーションの状態をリセットする関数
} = useMutation(mutationFn);
戻り値の使い方
//  const { data, isPending, error } = useQuery('todos', fetchTodos);
// 普通、上のようにuseQueryのカスタムフックを作って使用する。
   const { data, isPending, error } = useTodos();
// この書き方はよくない、todosが更新されるたびに<ul></ul>コンポーネントがアンマウントされる
//  if (isPending) return 'Loading...';
//  if (error) return 'An error has occurred: ' + error.message;

// データを表示
  return (
  <>
   {isPending ? 'Loading....' : ''}
   {error ? 'An error has occurred: '+ error.message : ''}
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  <>
  );

機能の確認

FRONT_END\src\pages\LoginPage.jsx
import { useLogin, useLoginUser, useLogout } from '../hooks/useAuthApi';

const LoginPage = () => {
// ユーザー情報はreact-queryでキャッシュされる
    const { data: loginUser = {} } = useLoginUser() || {};
    const { mutate: login, isPending } = useLogin();
    // error情報も取得できる
    const { mutate: logout,error:logoutError,reset:logoutReset } = useLogout();

// ので必要がない
//    const [loginUser, setLoginUser] = useState({});


    const onLogin = () => {
// useLogoutの戻り値をリセットする
        logoutReset();
    
        login({
            email: 'test@example.com',
            password: 'password',
        })

        // setLoginUser(user); // 不要
    }
    const onLogout = () => {
        // try{
        //     await api.logout()
        //     setLoginUser({});
        // }catch(error){
        // errorメッセージを取得してセットしていた
        //     setLoginUser(error.response.data);
        // }
        logout();
    }

    return (
        <div>
            <h1>ログインユーザー情報</h1>
            <button onClick={onLogin}>ログイン</button>
            <button onClick={onLogout}>ログアウト</button>
            <ul>
                {
                isPending ? <li>Loading.....</li>:
                Object.entries(loginUser).map(([key, value]) => {
                    return <li key={key}>{key}:{value}</li>
                })}
            </ul>
            {logoutError?  <p style={{color:'red'}}>{logoutError.response.data.message}</p>:''}
        </div>
    )
}
export default LoginPage

データの更新と一緒にコンポーネントが切り替わっていたらOK

loader関数の作成

loader関数の主な役割

  1. loaderは、特定のルートにアクセスする際に必要なデータをサーバーから非同期にロードしページに渡すデータフェッチとしての機能
  2. ユーザー認証が必要なページにアクセスする前に、ユーザーがログインしているかどうかをチェックしそうでない場合は、リダイレクトさせるガード機能
loder関数の使い方
// ユーザープロファイルデータをロードするloader関数
async function userProfileLoader() {
    // APIからユーザープロファイルデータを取得
    const userProfile = await fetchUserProfile();
// returnした戻り値は 登録した<element/>要素から取得できる
    return userProfile; 
}
// ルーター設定
export const router = createBrowserRouter([
    {
        path: 'profile',
        element: <UserProfilePage />,
        loader: userProfileLoader 
    },
    // 他のルート設定...
]);

// fetch等の副作用をコンポーネントから除外できる。
function UserProfilePage() {
// useLoaderDataフックを使用してloader関数のリターン値を取得できる
    const userProfile = useLoaderData();
    
    return (
        <div>
            <h1>ユーザープロファイル</h1>
            <p>名前: {userProfile.name}</p>
            <p>メール: {userProfile.email}</p>
        </div>
    );
}

ページにアクセスする前に非同期でデータを取得する

ため、

ページのレンダリングが開始される前にはデータが揃っている

ので、

ユーザーにスムーズなナビゲーション体験を提供できる

例えば、

ユーザーがダッシュボードページにアクセスしようとしたとき、

コンポーネントにアクセスする前にloader関数でユーザーの認証状態をチェックし、
認証されている場合はユーザーのプロフィールデータを取得してページに渡します。

これにより、コンポーネントが表示されるたときには、データが揃っているため、
アクセスと同時にユーザーに関連する情報が表示される

ため、

ページの読み込み中に発生する可能性のある遅延や不要なレンダリングを防ぐことができる。

また、

データの取得がルートレベルで行われるため、コンポーネントから副作用を除外し、データフェッチングをより効率的にするために役立ちます。
コンポーネントの状態を直接変更することなく、データをコンポーネントに提供する

ため、

これにより、コンポーネントは純粋な表示のロジックに集中でき、データフェッチングの複雑さが分離される

ので、

このアプローチは、特に大規模なアプリケーションやデータ依存の多いアプリケーションにおいて有効になる。

loder関数はページコンポーネントにアクセスするまえに実行される

ため、

データを取得して渡すだけではなく、
ページコンポーネントにアクセスする権利があるかどうかを判定して、ガードする役割も提供する。

しかし、

データの状態を管理してリダイレクトする機能はない。

ガードローダーの例
async function guardLoader() {
    if (!await isAuthenticated()) {
// ユーザーがログインしていない場合は、ログインページにリダイレクト
        throw redirect('/login'); //return ではなく throwが推奨されている
    }
    const userData = await getUserData();
    return userData;
}

この後、ログインページでログインして !await isAuthenticated()の状態が変化したら、
DashBoadPageに遷移させるという機能まではloader関数では書けない。

リダイレクトさせることで、ルーターを再評価することができる

  1. useNavigate()を用いしてlogin関数実行後、直接 DashBoadPageにredirectさせる(一般的)
  2. useStateでデータを管理して状態が更新されると、ルーターがその変更を検出し、適切なルートに再評価を行はせる
  3. ページの再読み込み等でもルーターは再評価される

loader関数の実装

FRONT_END\src\router.jsx
import { NavLink, Outlet, createBrowserRouter, redirect } from 'react-router-dom'
import LoginPage from './pages/LoginPage'
import DashboardPage from './pages/DashboardPage'
import { isLogin } from './api/authApi';

//+
/**
 * ログイン済みのみアクセス可能
 * loaderに渡すコールバック関数は非同期関数にする必要がある
 */
const guardLoader = async () => {
    const isLoggedIn = await isLogin();
    if(!isLoggedIn){
        throw redirect('/login');
    }
    return null;
};

// +
/**
 * ログインしていない場合のみアクセス可能
 * loaderに渡すコールバック関数は非同期関数にする必要がある
 */
const guestLoader = async () => {
    const isLoggedIn = await isLogin();
    if(isLoggedIn){
        throw redirect('/');
    }
    return null;
}

export const router = createBrowserRouter([
    {
        path: "/",
        element: <Layout />,
        children: [
            {
                path: 'login',
                element: <LoginPage />,
// +
                loader: guestLoader
            }, {
                path: '/',
                element: <DashboardPage />,
//+
                loader: guardLoader
            }
        ]
    }
])

function Layout() {
    return (
        <div>
            <Navbar />
            <Outlet />
        </div>
    )
};
function Navbar() {
    return (
        <header> 
            <nav>
                <ul>
                    <li>
                        <NavLink to="/">DashBoardPage</NavLink>
                    </li>
                    <li>
                        <NavLink to="/login">LoginPage</NavLink>
                    </li>
                </ul>
            </nav>
        </header>
    );
}
FRONT_END\src\pages\LoginPage.jsx
import { useLogin } from '../hooks/useAuthApi';

const LoginPage = () => {
    const { mutate: login } = useLogin();

    const onLogin = () => {
        login({
            email: 'test@example.com',
            password: 'password',
        })
    }
    return (
        <div>
            <h1>ログインページ</h1>
            <button onClick={onLogin}>ログイン</button>
        </div>
    )
}
export default LoginPage
FRONT_END\src\pages\DashboardPage.jsx
import { useNavigate } from "react-router-dom";
import { logout } from "../api/authApi";
import { useLoginUser } from "../hooks/useAuthApi";

const DashboardPage = () => {

    const { data: loginUser = {} } = useLoginUser() || {};
    const navigate = useNavigate();

    const onLogout = async () => {
       await logout();
        navigate('/login', { replace: true });
    }
    return (
        <div>
            <h1>ダッシュボード</h1>
            <button onClick={onLogout}>ログアウト</button>
            <ul>
                {Object.entries(loginUser).map(([key, value]) => {
                        return <li key={key}>{key}:{value}</li>
                })}
            </ul>
        </div>
    )
}

export default DashboardPage

ログイン、ログアウト処理が完了した後にリダイレクトさせて、ルートの再評価で、
今度はDashBoadPageにアクセスできる。

FRONT_END\src\hooks\useAuthApi.js
//+
import { useNavigate } from 'react-router-dom';

export const useLogin = () => {
    const queryClient = useQueryClient();

//+
    const navigate = useNavigate();
  
    const mutationOptions = {
      mutationFn: api.login,
      onError: (error) => {
        console.error('login error:', error);
      },
      
// 非同期処理完了後 + 成功したら 実行されるコールバック関数 
      onSuccess: (data) => {
        console.log('login success:', data);
        queryClient.setQueryData(['loginUserStatus'], data);

//+ 
// replace:true 現在のエントリを新しいもので置き換える
// ことでユーザーがブラウザの「戻る」ボタンをクリックした場合に、
// その前のページに戻ります。
// ユーザーが遷移前のページに戻ることが意図されていない場合に便利です。
        navigate('/', { replace: true });

      },
    };
    return useMutation(mutationOptions);
  };

export const useLogout = () => {
  const queryClient = useQueryClient();

//+
  const navigate = useNavigate();

  const mutationOptions = {
    mutationFn: api.logout,
    onError: (error) => {
      console.error('Logout error:', error);
    },

    onSuccess: (data) => {
      console.log('Logout success:', data);
      queryClient.setQueryData(['loginUserStatus'], {});

//+
      navigate('/login', { replace: true });
    },
  };

  return useMutation(mutationOptions);
};

loader関数の確認

ガードが効いているかの確認
ログイン後、ダッシュボードに遷移
ログアウト後、ログインページに遷移

loader関数とReact Queryの使い分け

  • loader関数: ルートレベルでのデータロードに使用し、ページコンポーネントがレンダリングされる前に必要なデータを提供します。これは、ページ遷移時のユーザーエクスペリエンスを向上させるために特に有効です

  • react-query: コンポーネントレベルでのデータ管理に使用し、データのキャッシング、同期化、および必要に応じた再取得を行います。これにより、データの一貫性を保ちながら、グローバルな状態管理の複雑さを軽減します

データの取得においては、同じデータをloader関数とreact-queryの両方で取得するのではなく、それぞれのツールが得意とする範囲で使用することが重要です。

例えば、認証データなどのルートレベルで一度だけ取得すればよいデータはloader関数で取得し、
コンポーネント間で共有される可能性のあるデータや頻繁に更新が必要なデータはreact-queryで管理します

ユーザー登録 機能の作成

/register ルートとナビリンクの作成

FRONT_END\src\router.jsx
//+
import RegisterPage from './pages/RegisterPage';

export const router = createBrowserRouter([
    {
        path: "/",
        element: <Layout />,
        children: [

//+
            {
                path: '/register',
                element: <RegisterPage />,
                loader: guestLoader
            }, 
        ]
    }
])
function Navbar() {
    return (
        <header>
            <nav>
                <ul>

{/* + */}
                    <li>
                        <NavLink to="/register">RegisterPage</NavLink>
                    </li>

Reactでフォームのデータを一括して送信する際、

非制御コンポーネントは、フォームのデータをDOMに直接依存させない、

ため、Reactの状態管理から一部解放され、フォームのパフォーマンスがあがる。

ただし、フォームのバリデーションや複雑な状態のロジックを扱う場合は、
制御コンポーネントを使用することが推奨されている

{/* value属性をつけないことで非制御コンポーネントになる */}
        <input id="name" name='name' type='text' required />

レジスターフォームの作成

FRONT_END\src\pages\RegisterPage.jsx
import { useState } from 'react';
import { useRegister } from '../hooks/useAuthApi';

export default function RegisterPage() {
    // const [formErrDate,setformErrDate] = useState({
    //     name:[],
    //     email:[],
    //     password:[]
    // });

    // mutate関数の戻り値はundefined。
    // mutationの結果を取得したい場合は、
    // Promiseを返すmutateAsync関数を使用する
    const {mutate:register,error:registerError} = useRegister();

    const {name:nameErr,email:emailErr,password:passwordErr} 
                    = registerError?.response?.data?.errors 
                                ?? {name:[],email:[],password:[]};

  const handleSubmit = e => {
    e.preventDefault();
    const formData = new FormData(e.target);
    register(formData,{
      // onErrorメソッドは呼び出し側で上書きできる
        // onError:(error)=>{
          // name,email,passwordに[]のデフォルト値を設定している
            // const newErrorDate = {name:[],email:[],password:[],...error.response.data.errors}
            // setformErrDate(newErrorDate);
        // },
    });
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="name">ユーザー名</label>
{/* value属性をつけないことで非制御型のコントロールフォームになる */}
        <input id="name" name='name' type='text' required />
        {/* {formErrDate.name.map(err=>{
            return <p style={{color:"red"}}>{err}</p>
        })} */}
        {nameErr ? nameErr.map(err=>{
            return <p key={err} style={{color:"red"}}>{err}</p>
        }):''}
      </div>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input id="email" name='email' type="email" required/>
        {/* {formErrDate.email.map(err=>{
            return <p style={{color:"red"}}>{err}</p>
        })} */}
        {emailErr ? emailErr.map(err=>{
            return <p key={err} style={{color:"red"}}>{err}</p>
        }):''}
      </div>
      <div>
        <label htmlFor="password">password</label>
        <input id="password" name='password' type="password"  required/>
        {/* {formErrDate.password.map(err=>{
            return <p style={{color:"red"}}>{err}</p>
        })} */}
        {passwordErr ? passwordErr.map(err=>{
            return <p key={err} style={{color:"red"}}>{err}</p>
        }):''}
      </div>
      <div>
        <label htmlFor="password_confirmation">password-confirmation </label>
        <input id="password_confirmation" name='password_confirmation' type="password"  required/>
      </div>
      <div>
        <button type="submit">登録</button>
      </div>
    </form>
  );
}

サーバーへのレジスターフォームの送信apiの作成

FRONT_END\src\api\authApi.js
//+
export const register = async (formDate) => {
    await http.get('/sanctum/csrf-cookie')
    // headerにtokenをつけて送信。
    const { data } = await http2.post('/register',formDate);
    return data
  }

作成したapiをreact queryで呼び出すフックの作成

FRONT_END\src\hooks\useAuthApi.js
//+
export const useRegister = () => {
    const queryClient = useQueryClient();
    const navigate = useNavigate();
  
    const mutationOptions = {
      mutationFn: api.register,
      onError: (error) => {
        console.error('login error:', error);
      },
      onSuccess: (data) => {
        console.log('login success:', data); // null
        
// dataがnullのためloginUserStatusを再フェッチさせてデータを取得させる        
// キャッシュを無効化して再度フェッチを強制する。
        queryClient.invalidateQueries(['loginUserStatus']);
        
        navigate('/', { replace: true });
      },
    };
  
    return useMutation(mutationOptions);
  };

ログインフォームの作成

// import * as api from '../api/authAPI'
import { useState } from 'react';
import { useLogin } from '../hooks/useAuthApi';

const LoginPage = () => {
    const { mutate: login } = useLogin();
    const [formErrDate, setformErrDate] = useState({
        email: [],
        password: []
    });

    const onLogin = (e) => {
        e.preventDefault();
        login({
            email: e.target.email.value,
            password: e.target.password.value,
        }, {
            onError: (error) => {
                const newErrorDate = { email: [], password: [], ...error.response.data.errors }
                setformErrDate(newErrorDate);
            }
        })
    }

    return (
        <div>
            <h1>ログインページ</h1>
            <form onSubmit={onLogin} noValidate>
                <div>
                    <label htmlFor="email">メールアドレス</label>
                    <input id="email" name='email' type="email" required />
                    {formErrDate.email.map(err => {
                        return <p style={{ color: "red" }}>{err}</p>
                    })}
                </div>
                <div>
                    <label htmlFor="password">password</label>
                    <input id="password" name='password' type="password" required />
                    {formErrDate.password.map(err => {
                        return <p style={{ color: "red" }}>{err}</p>
                    })}
                </div>

                <div>
                    <button type="submit">ログイン</button>
                </div>
            </form>
        </div>
    )
}
export default LoginPage

ユーザー登録の確認

ユーザー登録ができればOK
バリデーションが確認できればOK
登録したユーザーでログインできるか

パスワードリセット機能の作成

laravelでのテストメールの設定

登録して

設定を.envに張り付けたらいいだけ

.env
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=2(^_-)-☆3
MAIL_PASSWORD=8(^^)/($・・)/~~~0

larabelでのメール送信

  1. メイラブルクラスの作成
$
php artisan make:mail SendTestMail
BACK_END\app\Mail\SendTestMail.php

    public function content(): Content
    {
// viewsファイルを指定
// BACK_END\resources\views\testmail.blade.php
        return new Content('testmail');
    }
}
BACK_END\resources\views\testmail.blade.php
<p>send test email</p>
BACK_END\routes\web.php
Route::get('/mail',function(){
// メールアドレスは何でもいい
    Mail::to('xxxxx@example.com')->send(new SendTestMail());
    return '<h1>send test mail</h1>';
});

メールが送信できているか確認

http://localhost:8000/mail にアクセスして

MailtrapのinputBoxにメールが送信されていればOK

メールアドレスの送信フォームの作成

ルートとナビリンクの登録

FRONT_END\src\router.jsx
//+
import ResetPasswordPage from './pages/ResetPasswordPage';
import NewPasswordPage from './pages/NewPasswordPage';

export const router = createBrowserRouter([
    {
        path: "/",
        element: <Layout />,
        children: [
//+
            , {
                path: '/reset-password',
                element: <ResetPasswordPage />,
                loader: guestLoader
            },
            {
                path: 'password-reset/:token', // ダイナミックセグメント
                element: <NewPasswordPage />,
                loader: guestLoader
            },
        ]
    }
])

function Navbar() {
    return (
        <header>
            <nav>
                <ul>
{/* + メールアドレスを送信するページのリンク */}
                    <li>
                        <NavLink to="/reset-password">ResetPasswordPage</NavLink>
                    </li>
    );
}

メールアドレスと、新しいパスワードのサーバーへの送信apiの作成

FRONT_END\src\api\authApi.js
export const forgotPassword = async ({ email }) => {
  await http.get('/sanctum/csrf-cookie')
  // headerにtokenをつけて送信。

  const { data } = await http2.post('/forgot-password', { email });
  return data
}

export const resetPassword = async (formData) => {
  await http.get('/sanctum/csrf-cookie')
  // headerにtokenをつけて送信。

  const { data } = await http2.post('/reset-password', formData);
  return data
};

作成したapiをreqct-queryで取得する

useAuthApi.js
export const useForgotPassword = () => {

  const mutationOptions = {
    mutationFn: api.forgotPassword,
    onError: (error) => {
      console.error('login error:', error);
    },
    onSuccess: (data) => {
      console.log('login success:', data);
    },
  };

  return useMutation(mutationOptions);
};

export const useResetPassword = () => {
  const navigate = useNavigate();

  const mutationOptions = {
    mutationFn: api.resetPassword,
    onError: (error) => {
      console.error('login error:', error);
    },
    onSuccess: (data) => {
      console.log('login success:', data);

      navigate('/login', { replace: true });
    },
  };

  return useMutation(mutationOptions);
};

メールの送信フォームの作成

FRONT_END\src\pages\ResetPasswordPage.jsx
import { useState } from "react";
import { useForgotPassword } from "../hooks/useAuthApi";

const ResetPasswordPage = () => {
    const [errMessages, setErrMessages] = useState({ email: [] });
    const { mutate: forgotPassword, isPending, isSuccess, reset: resetPassword } = useForgotPassword();

    const onSendMail = (e) => {
        e.preventDefault();
        forgotPassword({
            email: e.target.email.value
        }, {onSuccess(){
            setErrMessages({email:[]});
        },
            onError: (err) => {
                const newErrors = { email: [], ...err.response.data.errors };
                setErrMessages(newErrors);
            }
        });
    };
    const onReForgotPassword = () => {
        resetPassword();
    };

    return (
        <div>
            <h1>Send Password Reset Emal</h1>
            {!isSuccess ?
                (!isPending ?
                    <form onSubmit={onSendMail}>
                        <label htmlFor="email">EMAIL:</label>
                        <input type="email" name="email" id="email" />
                        <button>リンクをメールに送る</button>
                        {errMessages.email.map(err => {
                            return <p style={{ color: 'red' }} key={err}>{err}</p>
                        })}
                    </form>
                    : 
                    <p>Sending.....</p>
                ):
                (<>
                    <p style={{ color: 'green' }}>メールアドレスにリセットリンクを送信しました。</p>
                    <button onClick={onReForgotPassword}>再送する</button>
                </>)
            }
        </div>
    )
}
export default ResetPasswordPage;

新しくパスワードを登録するページの作成

  1. このページにはメールのリンクをクリックしてlaravelのサーバーへ
  2. laravelのサーバーからブラウザにリダイレクト要求が送信されて
  3. ブラウザからこのページにアクセスされる

URLからクエリパラメータを取得するフックとURLパラメータを取得するフックをまとめるカスタムフックの作成

laravelが作成するデフォルトのURL http://frontドメイン/token/?email=xxx@xxxx.com
ここにブラウザにリダイレクトするよう通知する。

FRONT_END_bk_bk\src\hooks\useTokenAndEmail.js
/*
フックをまとめるときは単体で動作できる範囲内でまとめる
*/
import {
    useParams,
    useSearchParams
  } from "react-router-dom";
//関数名にuseとつけることで
export default function useTokenAndEmail() {
// コンポーネント以外で組み込みフックを呼び出すことができる関数を作成できる
    const { token } = useParams(); 
    const [searchParams] = useSearchParams();
    const email = searchParams.get('email');
    return { token, email };
}

フォームの作成

FRONT_END\src\pages\NewPasswordPage.jsx
import { useState } from "react";
import useTokenAndEmail from "../hooks/useTokenAndEmail";
import { useResetPassword } from "../hooks/useAuthApi";


const NewPasswordPage = () => {
    const [formErrDate, setformErrDate] = useState({
        password: []
    });
    useResetPassword
    const { mutate: resetPassword, isPending, isSuccess } = useResetPassword();

    const { token, email } = useTokenAndEmail();

    const handleSubmit = (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        resetPassword(formData,
            {
                onError: (error) => {
                    const newErrorDate = { password: [], ...error.response.data.errors }
                    setformErrDate(newErrorDate);
                }
            })
    };

    return (
        <div>
            <h1>New Password</h1>
            {
                !isSuccess ?
                    !isPending ?
                        (
                            <form onSubmit={handleSubmit} noValidate>
                                <div>
                                    <label htmlFor="password">password</label>
                                    <input id="password" name='password' type="password" required />
                                    {formErrDate.password.map(err => {
                                        return <p style={{ color: "red" }}>{err}</p>
                                    })}
                                </div>
                                <div>
                                    <label htmlFor="password_confirmation">password-confirmation </label>
                                    <input id="password_confirmation" name='password_confirmation' type="password" required />
                                </div>
                                <div>
                                    <button type="submit">登録</button>
                                </div>
                                <input type="hidden" name="token" value={token} />
                                <input type="hidden" name="email" value={email} />
                            </form>)
                        :
                        <p>Sending....</p>
                    : ''
            }
        </div>
    )
}
export default NewPasswordPage

機能の確認

メールアドレスをサーバーに送信できる
サーバーからメールが届く
リンクのサーバー側のアドレスにアクセスするとNewPasswordPageにリダイレクトされる
パスワードのリセットができる
新しいパスワードでログインできたらOK

メール検証済みユーザーを作成する

サーバー側

検証済みユーザーかどうかを通知するルートの作成

BACK_END\routes\auth.php
// + 
Route::get('/verification-check', [EmailVerificationNotificationController::class, 'isVerifiedEmail'])
    ->middleware(['auth','verified'])
    ->name('verification.check');
  • メールを送るクラスの修正
    • 検証済みユーザーがアクセスしてきたら
      サーバー側のRouteServiceProvider::HOMEにリダイレクトする
      のを変更し
      検証済みというjsonのエラーメッセージを送る
    • 検証済みユーザーかどうかを判定するメソッドの追加
BACK_END\app\Http\Controllers\Auth\EmailVerificationNotificationController.php
class EmailVerificationNotificationController extends Controller
{
    public function store(Request $request): JsonResponse|RedirectResponse
    {
        if ($request->user()->hasVerifiedEmail()) {
// 修正
            // return redirect()->intended(RouteServiceProvider::HOME);
            return response()->json(['error' => 'Email is already verified.'], 400);
        }

        $request->user()->sendEmailVerificationNotification();

        return response()->json(['status' => 'verification-link-sent']);
    }

// +
    public function isVerifiedEmail(Request $request){
        if ($request->user()->hasVerifiedEmail()) {
            return true;
        }
        return false;
    }
}

メールのリンクからアクセスされるルートのクラスの確認

BACK_END\app\Http\Controllers\Auth\VerifyEmailController.php
<?php
class VerifyEmailController extends Controller
{
    public function __invoke(EmailVerificationRequest $request): RedirectResponse
    {
// 既に検証済みユーザーなら
        if ($request->user()->hasVerifiedEmail()) {

// フロント側のRouteServiceProvider::HOMEにクエリをつけてリダイレクトするようブラウザに通知する
            return redirect()->intended(
                config('app.frontend_url').RouteServiceProvider::HOME.'?verified=1'
            );
        }
// 検証済みユーザーになれたら この条件文で検証済みユーザーになる or なれない
        if ($request->user()->markEmailAsVerified()) {
    // なれたら、
    // イベントを発火させる Verifiedオブジェクトのユーザープロパティにauthユーザーを登録して発火
            event(new Verified($request->user()));
            // Verifiedイベントのリスナーにはデフォルトではなにも登録されていない
        }

// なれてもなれなくても、最後は フロント側のRouteServiceProvider::HOMEに
// クエリをつけてリダイレクトするようブラウザに通知している
        return redirect()->intended(
            config('app.frontend_url').RouteServiceProvider::HOME.'?verified=1'
        );
    }
}

フロント側

サーバに登録したメールにメールを送るよう通知するアクションボタンをDashboardPageに作成

FRONT_END\src\pages\DashboardPage.jsx
import { useNavigate,useLoaderData } from "react-router-dom";
import { logout } from "../api/authApi";
import { useLoginUser, useVerificationEmail } from "../hooks/useAuthApi";

const DashboardPage = () => {

//+ 既に検証済みのユーザーかどうかをloaderで取得したのを取得
    const isVrifiedEmail = useLoaderData();

    const { data: loginUser = {} } = useLoginUser() || {};
    const navigate = useNavigate();

//+ サーバーにそのアクションを 送る、通信中、成功 
    const { mutate: verificationEmail, isPending, isSuccess } = useVerificationEmail();

    const onLogout = async () => {
        await logout();
        navigate('/login', { replace: true });
    }

//+ ボタンを押したら送る
    const onSendEmail = async () => {
        verificationEmail();
    }
    
    return (
        <div>
            <h1>ダッシュボード</h1>
            <button onClick={onLogout}>ログアウト</button>
{/* + */}
            {!isVrifiedEmail ?
                !isSuccess ?
                    !isPending ? (
                        <button onClick={onSendEmail}>メール検証がまだ完了していません。クリックして検証メールを送信してください。</button>
                    ) :
                        <p>Sending...</p>
                    : (
                        <p style={{ color: 'green' }}>検証メールが送信されました。メール内のリンクをクリックして検証を完了させてください。</p>
                    ) : <p>検証済みユーザー</p>}

                    
            <ul>
                {Object.entries(loginUser).map(([key, value]) => {
                    return <li key={key}>{key}:{value}</li>
                })}
            </ul>
        </div>
    )
}

export default DashboardPage

ガードローダーの修正

FRONT_END\src\router.jsx
//+ isVerifiedEmail の追加
import { isLogin, isVerifiedEmail } from './api/authApi';

const guardLoader = async () => {
    const isLoggedIn = await isLogin();
    if (!isLoggedIn) {
        throw redirect('/login');
    }
    return null;
};

// +
const isVerifiedEmailGuardLoader = async () => {
    await guardLoader();
    return await isVerifiedEmail();
};

export const router = createBrowserRouter([
    {
        path: "/",
        element: <Layout />,
        children: [
            {
                path: 'login',
                element: <LoginPage />,
                loader: guestLoader
            }, {
                path: '/',
                element: <DashboardPage />,
// 修正
                // loader: guardLoader
                loader: isVerifiedEmailGuardLoader
            },

サーバーとの通信

FRONT_END\src\api\authApi.js
// + メールを送るようサーバーに通知する
export const verificationEmail = async (formData) => {
  await http.get('/sanctum/csrf-cookie')
  // headerにtokenをつけて送信。

  const { data } = await  http2.post('/email/verification-notification');
  return data
};

// 検証済みユーザーかどうかの判定をサーバーから取得
export const isVerifiedEmail = async () => {
  const { data } = await  http.get('/verification-check');
  return data
};

作成したapiをreqct-queryで発火させる

FRONT_END\src\hooks\useAuthApi.js
export const useVerificationEmail = () => {

  const mutationOptions = {
    mutationFn: api.verificationEmail,
    onError: (error) => {
      console.error('login error:', error);
    },
    onSuccess: (data) => {
      console.log('login success:', data);

    },
  };

  return useMutation(mutationOptions);
};

機能の確認
アクションボタンを押す
届いたメールのリンクをクリックする リンクのあて先はサーバー
サーバーからダッシュボードにリダイレクトされる
ダッシュボードに検証済みユーザーとでていればOK

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0