1.始めに
フロントにReactやVueといったJSフレームワーク、バックにDjangoやLaravel、Rails等を用いてSPA(Single Page Application)を構築するのがWeb界隈では主流になりつつあります。SPAの形にすることで静的ページの読み込み処理の必要がなくなり、処理が早くなったりする分今後も流れは加速していくことと思われます。
そうなってきますと、当然のことながら認証機能についてもフロントでフォームを作成してバックエンドでAPI認証をすることが必要になってきます。
ですが、いざ作ろうとしたら世にいろいろなSPAが出回っているにも関わらず、API認証構築についての記事とかが少なく、ReactとLaravelでAPI認証を作ろうとして思いのほか構築に苦労しましたので備忘録として今回記事を書くことにしました。
2.Laravelアプリの作成と下準備
・OS:Windows
・xamppインストール済み
・composerインストール済み
・node.jsインストール済み
・VScodeインストール済み
・環境変数でPHPのパスを追加済み
上記の条件を満たし、コマンドプロンプトでcomposerコマンドとnpmコマンドとphp artisanコマンドが使えることが前提で話を進めます。
2-1.プロジェクト作成
composerコマンドを叩いてプロジェクトを新規に作成します。
composer create-project laravel/laravel=8.* sample --prefer-dist
laravelコマンドが使えるなら以下のコマンドでも可です。
laravel new sample
sampleの部分はプロジェクト名です。好きなように書き換えてもらって構いません。私はLaravel8で実装しましたのでバージョンを8にしています。
2-2.Laravel UIの導入とReactの導入、通常の認証用のテンプレート導入
プロジェクトが作成されたらVScodeで作成したプロジェクトを開きます。そしてターミナルを呼び出して下記コマンドを順次打ち込み実行します。
composer require laravel/ui
php artisan ui react --auth
npm install && npm run dev
一番上のコマンドはLaravel8に対応したLaravel UIの導入に使っています。2番目でReactを導入しています(API実装に際して--authは必要ありませんが今回は見慣れたLaravelのトップページをカスタマイズすることを意識してますのでつけました)。3番目のコマンドはLaravelアプリ内でJSライブラリをインストールしたりReactをビルドするためのコマンドです。&&が認識されない時にはnpm installとnpm run devを分けて実行することをおすすめします。
2-3.React-Router,React-Router-Domの導入
SPAの特徴として、静的ページの読み込みがないことについては最初に触れたことですが、ルーティングをフロントエンドで定義することでクライアントサイドでHTMLを動的に書き換えてあたかもページ遷移しているかのように見せています。Reactでルーティングを定義する場合、React-Router-Domを定義する必要がありますので、これを導入していきます。
npm install react-router-dom@5.*
バージョンを指定しない場合最新のReact-Router-Dom v6がインストールされますが、v5とv6ではルーティングの書き方が違ってきますので、既出の情報が多いv5を指定してルーティングを書いた方が確実です。
2-4.既存のファイルの変更
次に、プロジェクト作成後作られたファイルについてReactが有効になるように書き換えていきます。2-2で実行したコマンドにより、プロジェクト内のresourcesディレクトリ内のviewsディレクトリにはauthやlayoutsディレクトリが生成されているはずです。まずはlayoutsディレクトリ内のapp.blade.phpをいじります。
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>app.blade.php</title>
<!-- Scripts -->
<!-- ここがポイント-->
<script src="{{ asset(mix('js/app.js')) }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
@yield('style')
</head>
<body>
<div id="app">
@yield('content')
</div>
@yield('script')
</body>
</html>
ポイントは通常publicディレクトリのapp.jsを読み込むところを、resourcesディレクトリのjsディレクトリにあるapp.jsをミックスして読み込むところです。これによってReactコンポーネントを呼び出すことができるようになります。
次に、index.blade.phpを作り、それをいじります。
@extends('layouts.app')
@section('content')
<div id="nav"></div><!--Reactコンポーネントの呼び出し箇所-->
@endsection
ここではレンダリングするためのIDをnavにしていますがID名は何でも構いません。exampleにするとReact導入時に自動で作られるExample.jsが表示されますので不慣れな方はそちらでもいいかなと思います。
次に、Reactのルーティングを有効にするためにweb.phpを変更します。
Route::get('/{any}', function () {
return view('index');
})->where('any', '.*');
デフォルトでは'/'のルーティングですとwelcome.blade.phpが指定されてるわけですが、そこをindex.blade.phpが返るように書き換えるのと、Reactのルーティングに合わせてレンダリングが行われるように上記のような書き方をしています。ここについてはなぜこういう書き方をする必要があるか私にもわからないとこですので知ってる人いましたら是非記事書いてください。
そのことはどうでもいいとして、次に、React導入時に自動で生成されるExample.jsをApp.jsにリネームしてこれをフロントエンドのコアとして使っていくこととします。詳細なコードについてはAPI認証の実装に際して書いていきますのでここでは割愛します。拡張子をjsのままにするかjsxにするかはお任せします(私はReactのロゴが気に入っているためわざとjsxに変更しました。jsのままでも動きますので問題ありません)。
Example.jsをApp.jsに変更しましたので関連するファイルを修正します。resource/jsディレクトリ直下にapp.jsがありますので、require('./components/Example');の箇所をrequire('./components/App');に変更します。
/**
* First we will load all of this project's JavaScript dependencies which
* includes React and other helpers. It's a great starting point while
* building robust, powerful web applications using React + Laravel.
*/
require('./bootstrap');
/**
* Next, we will create a fresh React component instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
require('./components/App');// ここが変更箇所
2-5.コンポーネントの作成
とりあえずApp.jsxとTop.jsx、About.jsx、GlobalNav.jsxコンポーネントを用意します。React-Router-Domが正常に動くかどうかさえ確認できればいいのでこの段階ではそこまで込み入ったコードにはなりません。
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Route, Switch, Link} from 'react-router-dom';
import GlobalNav from './GlobalNav';
import Top from './Top';
import About from './About';
function App(){
return(
<BrowserRouter>
<GlobalNav />
<Switch>
<Route exact path="/">
<Top />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</BrowserRouter>
)
}
if (document.getElementById('nav')) {
ReactDOM.render(<App />, document.getElementById('nav'));
}
React-Router-Dom v5を採用する場合はこのような書き方になります。v6ですと書き方がまるっきり違いますのでこの書き方しても動きません。指定されたルーティングをフロントサイドで叩くと対象のコンポーネントが呼び出され、id="nav"となっている箇所をレンダリングすることになります。他のコンポーネントもシンプルなものでいいです。ルーティングの動作確認用ですので。
import React from 'react';
const Top = () => {
return <h1>TOP</h1>
}
export default Top;
import React from 'react';
const About = () => {
return <h1>ABOUT</h1>
}
export default About;
import React from 'react';
import {Link} from 'react-router-dom';
function GlobalNav () {
return(
<ul>
<li>
<Link to="/">
<span>Top</span>
</Link>
</li>
<li>
<Link to="/about">
<span className="nav-title">About</span>
</Link>
</li>
</ul>
)
}
export default GlobalNav;
これでSPAのルーティング部分が正常に動き、メニューリストのTopを押したらTOPの文字が、Aboutを押したらABOUTが出力されたら下準備は完了です。
3.Laravel8におけるAPI認証
Laravel8のドキュメントを確認してみますと、SPA用のAPI認証としては、PassportとSanctumが提供されているらしいです。そのうちのSanctumを用いた認証の実装を今回は行いました。フロントとバックの実装をしていく前に下準備として以下のコマンドをターミナルで実行します。
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
1つ目のコマンドはsanctumの追加、2つ目のコマンドでSanctumServiceProviderをLaravelに追加、そしてマイグレーションとなります。マイグレーション前にはphpMyAdminに.envファイルで設定したDB名と全く同じDBを予め用意しておきましょう(初歩的なことですが結構忘れられやすいですので…)。
コマンドの実行が終わったら各種ファイルを確認して設定が変更されているかどうかを確認します。まず、Kernel.phpです。31行目から47行目のあたりを探しますと、以下のような記述があります。
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // コメントアウト状態だったらコメントアウトを外す、なかったら書き足す
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Kernel.phpに\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestAreStateful::class,があるかどうかを調べます。大抵コメントアウトされた状態で追加されてると思いますがなかったらこれを'api' => [],の中に書き足してください。
次にUser.phpを確認します。まず13行目にHasApiTokensが追記されているかどうかを確認します。そして、use Laravel\Sanctum\HasApiTokens;が追記されているかどうかも確認します。欠けているものがあったら書き足しましょう。最終的に以下の形になっていればOKです。
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; // なければ書き足す
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable; // HasApiTokensがなかったら書き足す
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
それから、configディレクトリのcors.phpについても32行目を確認する必要があります。ここがデフォルトのまま(false)ですとAPI認証に失敗します。
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // falseだったらtrueに変更
];
後はターミナルで以下のコマンドを実行し、必要なJSライブラリを予めインストールしておきます。
npm install axios
npm install sweetalert
sweetalertは参考にした動画で使われていましたのでそれに倣って使いましたがこれについては別になくても大丈夫です。ただ、ビジュアルとかアニメーションが凝っていますので私的にはおすすめです。
3-1.フロントエンドの実装
参考にしたYouTubeの動画に忠実にユーザー登録とログインの機能は実装しました。フロントエンドのコードの全貌は以下の通りになります。
import React, { useState } from "react";
import axios from 'axios';
import swal from 'sweetalert';
import { useHistory } from 'react-router-dom';
function Register() {
const history = useHistory();
const [registerInput, setRegister] = useState({
name: '',
email: '',
password: '',
error_list: [],
});
const handleInput = (e) => {
e.persist();
setRegister({...registerInput, [e.target.name]: e.target.value });
}
const registerSubmit = (e) => {
e.preventDefault();
const data = {
name: registerInput.name,
email: registerInput.email,
password: registerInput.password,
}
axios.get('/sanctum/csrf-cookie').then(response => {
axios.post(`/api/register`, data).then(res => {
if(res.data.status === 200){
localStorage.setItem('auth_token', res.data.token);
localStorage.setItem('auth_name', res.data.username);
swal("Success", res.data.message, "success");
history.pushState('/')
} else {
setRegister({...registerInput, error_list: res.data.validation_errors});
}
});
});
}
return (<div className="container">
<div className="row justify-content-center">
<div className="col-md-6 col-lg-6 mx-auto">
<div className="card">
<div className="card-header">
<h4>Register</h4>
</div>
<div className="card-body">
<form onSubmit={registerSubmit}>
<div className="form-group mb-3">
<label>User Name</label>
<input type="" name="name" onChange={handleInput} value={registerInput.name} className="form-control" />
<span>{registerInput.error_list.name}</span>
</div>
<div className="form-group mb-3">
<label>Mail Address</label>
<input type="" name="email" onChange={handleInput} value={registerInput.email} className="form-control" />
<span>{registerInput.error_list.email}</span>
</div>
<div className="form-group mb-3">
<label>Password</label>
<input type="" name="password" onChange={handleInput} value={registerInput.password} className="form-control" />
<span>{registerInput.error_list.password}</span>
</div>
<div className="form-group mb-3">
<button type="submit" className="btn btn-primary">Register</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
export default Register
新規ユーザー登録(Register)の場合は上記の書き方をすることで最低限の機能を実装できるみたいです。
特徴としては、まず各フォームの値とerror_listを宣言した後初期化、各inputタグの中にonChange={handleInput},value={registerInput.name}等を埋め込むことで、各フォームに入力された値をdata変数の中に送り込み、axiosを使ってCSRF検証を行い、バックエンド側に情報を飛ばす、そして、バックエンド側で返されたレスポンスのステータスによって表示結果を変えるというのが流れになってきます。これだけ見てても普通にLaravelのAuthを使うのと比較するとはるかにめんどくさそうというイメージがわきます。
import React, { useState } from "react";
import swal from "sweetalert";
import { useHistory } from 'react-router-dom';
import axios from 'axios';
function Login() {
const history = useHistory();
const [loginInput, setLogin] = useState({
email: '',
password: '',
error_list: [],
});
const handleInput = (e) => {
e.persist();
setLogin({...loginInput, [e.target.name]: e.target.value});
}
const loginSubmit = (e) => {
e.preventDefault();
const data = {
email: loginInput.email,
password: loginInput.password,
}
axios.get('/sanctum/csrf-cookie').then(response => {
axios.post(`api/login`, data).then(res => {
if(res.data.status === 200){
localStorage.setItem('auth_token', res.data.token);
localStorage.setItem('auth_name', res.data.username);
swal("ログイン成功", res.data.message, "success");
history.push('/');
location.reload();
} else if (res.data.status === 401){
swal("注意", res.data.message, "warning");
} else {
setLogin({...loginInput, error_list: res.data.validation_errors});
}
});
});
}
return (<div className="container">
<div className="row justify-content-center">
<div className="col-md-6 col-lg-6 mx-auto">
<div className="card">
<div className="card-header">
<h4>Login</h4>
</div>
<div className="card-body">
<form onSubmit={loginSubmit}>
<div className="form-group mb-3">
<label>Mail Address</label>
<input type="email" name="email" onChange={handleInput} value={loginInput.email} className="form-control" />
<span>{loginInput.error_list.email}</span>
</div>
<div className="form-group mb-3">
<label>Password</label>
<input type="password" name="password" onChange={handleInput} value={loginInput.password} className="form-control" />
<span>{loginInput.error_list.password}</span>
</div>
<div className="form-group mb-3">
<button type="submit" className="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
export default Login;
こちらはログインフォームになります。こちらもRegisterと処理の流れはほとんど変わりません。違いがあるとしたら、認証に成功した時に
localStorage.setItem('auth_token', res.data.token);
localStorage.setItem('auth_name', res.data.username);
この2つの処理を挟むことで今ユーザーがログイン状態であることをReactに認識させている点です。認証成功後location.reload()で再読み込みをかけている箇所はセッションの状態を反映させるためです。参考にした動画では認証成功後瞬時にログイン後の出力内容が反映されていましたが私の場合はそれがうまくいきませんでしたのでこの処理を追記しています。原因があるとすれば、'/'でトップページを再レンダリングしてもその範囲外のコンポーネントについてはログイン前の情報から変更がされないことが原因だろうなとは推測しています。ページ全体を再レンダリングする設計の場合は再読み込みの処理は抜いてしまっても一向に問題ありません。むしろ抜いたほうがいいです。
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Route, Switch, Link} from 'react-router-dom';
import GlobalNav from './GlobalNav';
import Top from './Top';
import About from './About';
import Register from './Register';
import Login from './Login';
import axios from 'axios';
axios.defaults.baseURL = "http://localhost:8000/";
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.headers.post['Accept'] = 'application/json';
axios.defaults.withCredentials = true;
axios.interceptors.request.use(function(config){
const token = localStorage.getItem('auth_token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
});
function App(){
return(
<BrowserRouter>
<GlobalNav />
<Switch>
<Route exact path="/">
<Top />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/register">
<Register />
</Route>
<Route path="/login">
<Login />
</Route>
</Switch>
</BrowserRouter>
)
}
if (document.getElementById('nav')) {
ReactDOM.render(<App />, document.getElementById('nav'));
}
API認証実装に合わせてApp.jsxも大きく変更になります。まず当たり前のことですがログインと新規登録のルーティングを追加しています。そして、上の方にaxios.~といろいろなコードが羅列されている箇所が追加されました。こちらはbaseURLを定義したりトークンを発行したりとめんどくさそうなことしてそうな感じがしますが、要点をまとめてしまうとログイン中のアカウントの認証トークンをここで保持するように仕向けてあるわけです。
import React from 'react';
import {Link, useHistory} from 'react-router-dom';
import axios from 'axios';
import swal from 'sweetalert';
function GlobalNav () {
const history = useHistory();
const logoutSubmit = (e) => {
e.preventDefault();
axios.post(`/api/logout`).then(res => {
if (res.data.status === 200) {
localStorage.removeItem('auth_token', res.data.token);
localStorage.removeItem('auth_name', res.data.username);
swal("ログアウトしました", res.data.message, "success");
history.push('/');
location.reload();
}
});
}
var AuthButtons = '';
if (!localStorage.getItem('auth_token')){
AuthButtons = (
<li>
<Link to="/register">
<span>Register</span>
</Link>
</li>
<li>
<Link to="/login">
<span>Login</span>
</Link>
</li>
);
} else {
AuthButtons = (
<li>
<div onClick={logoutSubmit}>
<span className="text-white">ログアウト</span>
</div>
</li>
);
}
return(
<ul>
<li>
<Link to="/">
<span>Top</span>
</Link>
</li>
<li>
<Link to="/about">
<span>About</span>
</Link>
</li>
{AuthButtons}
</ul>
)
}
export default GlobalNav;
GlobalNav.jsxも認証状態によってメニューに表示される内容が変化するようにしました。ログアウト処理の時にはルーティングの設定がいらないっていうのが意外なところですね。api通信をする際に情報を飛ばしたりはしますけれどもフロント側でルーティングを設定していないあたりが新鮮に思えました。localStorage.getItem('auth_token')がLaravelで認証した場合の@guestに対応しているところかなあと思いました。こちらもログアウト処理後にページ再読み込みの処理をかけたりする箇所ありますが、ページ全体をレンダリングするようなものでしたらここの処理不要になります。
3-2.バックエンドの実装
続いてバックエンドについてもコードを書いていきます。
まずは、api.phpにAPI用のルーティングを設定することから始めます。API認証に必要になってくるのはregister, login, logoutの3つになってきますので、それらを記述していきます。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\API\AuthController;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function() {
Route::post('logout', [AuthController::class, 'logout']);
});
api.phpにルーティングを追記したらControllerを作成します。ターミナルで以下のコマンドを叩いてコントローラーを作成しましょう。
php artisan make:controller API/AuthController
コントローラーが生成されたら中身のコードを次のようにします。
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Validator;
class AuthController extends Controller
{
public function register(Request $request){
$validator = Validator::make($request->all(), [
'name'=>'required|max:191',
'email'=>'required|email|max:191|unique:users,email',
'password'=>'required|min:8',
]);
if($validator->fails()){
return response()->json([
'validation_errors'=>$validator->messages(),
]);
} else {
$user = User::create([
'name'=>$request->name,
'email'=>$request->email,
'password'=>Hash::make($request->password),
]);
$token = $user->createToken($user->email.'_Token')->plainTextToken;
return response()->json([
'status'=>200,
'username'=>$user->name,
'token'=>$token,
'message'=>'Registerd Successfully'
]);
}
}
public function login(Request $request) {
$validator = Validator::make($request->all(), [
'email'=>'required',
'password'=>'required',
]);
if ($validator->fails()){
return response()->json([
'validation_errors'=>$validator->messages(),
]);
} else {
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json([
'status'=>401,
'message'=>'入力情報が不正です',
]);
} else {
$token = $user->createToken($user->email.'_Token')->plainTextToken;
return response()->json([
'status'=>200,
'username'=>$user->name,
'token'=>$token,
'message'=>'ログインに成功しました。'
]);
}
}
}
public function logout(){
auth()->user()->tokens()->delete();
return response()->json([
'status'=>200,
'message'=>'ログアウト成功',
]);
}
}
REST APIとかLaravelを用いたAPI開発をやったことがある方ないしは、ajaxを使ったことがある方からしたらなじみ深い書き方かもしれません。コントローラーの中でしている処理としてはフォームに入力された情報の整合性を確認し、パスワードをハッシュ化してDBにinsert、認証成功時にはログインユーザーの情報や認証トークンをJSON形式で返す、何かしらのエラーとかバリデーションに失敗した場合とかにはそれに対応したJSONデータを返す、ただそれだけです。
あとはこれをフロントエンドで返されたデータを利用して出力内容を変えているだけになります。からくりがわかると簡単ですね?
4.まとめ
以上の処理を書いていただくことでAPI認証の一番基本的な形は出来上がると思います。あとはフロントエンドのデザインを変えたりとかバックエンドの処理をさらに複雑にしていくことでいろいろとカスタマイズできるのではないかと思います。Laravel+ReactでのAPI認証でしたらこれでうまくいきましたがRails apiモードとかDjango Rest Frameworkだとこのあたりの仕様どうなってるんでしょうね?気が向いたらそっちも調べてみようかなと思います。
もし本記事通りにやってみて動かなかった場合はnpm run devコマンドでフロントエンドの再ビルドをしたり下記の参考サイトや参考動画で調べていただければと思います。そのうえで本記事に記入漏れとかありましたら修正依頼とか送っていただければ手の空いているときに加筆修正します。
5.参考サイト、参考動画
LaravelではじめるReact.jsのSPAアプリ開発 (1)
LaravelにReactを導入する時の手順〜ルーティングの設定
yarnで古いバージョンのパッケージをインストールする方法メモ
Laravel 8.x 認証
ReactJS Ecommerce Part 3: Complete Registration System with API in React JS using Laravel 8 Sanctum←この動画シリーズに特に助けられました。英語に抵抗のない方は是非視聴をおすすめします!