Pada tutorial ini akan membahas langkah demi langkah untuk membangun sistem autentikasi Single Page Application (SPA) yang aman dan modern. Namun saya sarankan untuk membaca tutorial sebelumnya tentang membuat aplikasi todo dengan Laravel dan React, agar lebih memahami kombinasi basic dari Laravel plus React.

Dalam tutorial ini, Versi yang kita gunakan:

  • Laravel: Tutorial ini dibuat dengan Versi 12
  • React:  Menggunakan JavaScript (bukan Typescript)
  • Tailwind CSS: v3.x

Bagian 1: Persiapan Backend Restful API dengan Laravel

Buat Projek Baru Laravel:

Gunakan Composer dengan perintah berikut:

composer create-project laravel/laravel spa-laravel-backend
cd spa-laravel-backend

Konfigurasi Database:

Buka file .env dan sesuaikan konfigurasi database:

​DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nama_database
DB_USERNAME=username
DB_PASSWORD=password

Install Laravel Sanctum:

Sanctum akan menangani autentikasi API berbasis token dan autentikasi SPA berbasis session.

composer require laravel/sanctum

Publikasikan file konfigurasi dan migrasi Sanctum dengan perintah berikut:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Jalankan Migrasi: Ini akan membuat tabel users, password_reset_tokens, failed_jobs, personal_access_tokens, dan tabel migrasi Sanctum.

php artisan migrate

Konfigurasi Kernel HTTP untuk Middleware Sanctum:

Buka bootstrap/app.php. Pastikan middleware EnsureFrontendRequestsAreStateful dari Sanctum ada di grup middleware api. Middleware ini untuk autentikasi SPA Sanctum karena membantu Laravel mengidentifikasi request dari frontend SPA.

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Cache\RateLimiting\Limit; 
use Illuminate\Http\Request;             
use Illuminate\Support\Facades\RateLimiter; 

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        // Tambahkan konfigurasi untuk rate limiting di sini
        using: function () { // <-- Ini adalah cara untuk menambahkan konfigurasi rute lebih lanjut
            // Rate Limiter untuk API
            RateLimiter::for('api', function (Request $request) {
                return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
            });
            // Muat file rute
            Route::middleware('web')
                ->group(base_path('routes/web.php'));

            Route::middleware('api') // Middleware 'api' yang akan menggunakan throttle:api
                ->prefix('api')
                ->group(base_path('routes/api.php'));
        }
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->group('api', [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api', // <-- Ini yang menyebabkan error jika 'api' tidak terdefinisi
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ]);

        // ... middleware lainnya
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // ...
    })->create();

Konfigurasi CORS (Cross-Origin Resource Sharing):

Frontend React akan berjalan pada domain/port yang berbeda dari backend Laravel. Oleh karena itu, kita membutuhkan CORS.

Secara default, laravel sudah tersedia CORS, kita hanya perlu aktifkan CORS dengan perintah berikut:

php artisan config:publish cors

Buka config/cors.php.  dan isi file sebagai berikut:

<?php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout', 'register'], // Tambahkan rute autentikasi
    'allowed_methods' => ['*'],
    'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:5173')], // Ambil dari .env
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'], // Atau spesifik seperti 'Content-Type', 'X-XSRF-TOKEN', 'Authorization', 'Accept', 'X-Requested-With'
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true, // PENTING untuk Sanctum SPA
];

Tambahkan FRONTEND_URL ke file .env:

FRONTEND_URL=http://localhost:5173
SANCTUM_STATEFUL_DOMAINS=localhost:5173
SESSION_DOMAIN=localhost 
SESSION_DRIVER=cookie
SESSION_SECURE_COOKIE=false

SANCTUM_STATEFUL_DOMAINS: Daftar domain frontend yang akan menggunakan autentikasi stateful SPA Sanctum.

SESSION_DOMAIN: Penting agar cookie sesi dapat diakses dengan benar oleh frontend, terutama jika berada di subdomain yang berbeda. Dalam tahap development, kita isi dengan localhost, set ke localhost.

Update Model User:

Pastikan model User (app/Models/User.php) menggunakan trait HasApiTokens dari Sanctum.

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; 
}

Definisikan Route Autentikasi (API):

Buka routes/api.php dan tambahkan rute untuk registrasi, login, logout, dan mengambil data pengguna.

<?php

// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController; // Kita akan buat Controller ini

// Laravel Sanctum secara otomatis menyediakan rute /sanctum/csrf-cookie
// yang akan kita panggil dari frontend sebelum request state-changing.

Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
    Route::post('/logout', [AuthController::class, 'logout']);
});

Buat AuthController untuk API:

Buat controller yang akan menangani logika autentikasi.

php artisan make:controller Api/AuthController

Isi app/Http/Controllers/Api/AuthController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Http\JsonResponse;

class AuthController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        Auth::login($user); // Langsung login setelah registrasi (opsional tapi umum untuk SPA)

        return response()->json($user, 201);
    }

    public function login(Request $request): JsonResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials, $request->boolean('remember'))) {
            $request->session()->regenerate(); // Penting untuk keamanan
            return response()->json(Auth::user());
        }

        return response()->json([
            'message' => 'Email atau password salah.', // Pesan error yang lebih umum
        ], 401); // Unauthorized
    }

    public function logout(Request $request): JsonResponse
    {
        Auth::guard('web')->logout(); // Pastikan menggunakan guard 'web' untuk SPA

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return response()->json(['message' => 'Berhasil logout']);
    }
}

Untuk autentikasi SPA, Sanctum menggunakan session guard (web) Laravel. Oleh karena itu, saat logout, kita menggunakan Auth::guard('web')->logout().

Bagian backend. Sudah selesai. Sekarang kita akan melanjutkan ke sisi React (Frontend)

 

Bagian 2: Persiapan Frontend dengan React (JavaScript) & Tailwind CSS v3 

Buat Proyek React Baru dengan Vite:

npm create vite@latest spa-react-frontend -- --template react
cd spa-react-frontend
npm install

Ini akan membuat proyek React dengan JavaScript.

Install Tailwind CSS v3: 

Ikuti langkah-langkah resmi untuk Tailwind CSS v3 dengan Vite.

npm install -D tailwindcss@3
npx tailwindcss init -p

Perintah ini akan membuat dua file konfigurasi: tailwind.config.js dan postcss.config.js.

Kemudian Konfigurasi tailwind.config.js: Dalam content mencakup semua file. dalam hal ini kita menggunakan ekstensi js dan jsx. tapi kita juga isi ts (typescript) atau boleh dihilangkan.

// tailwind.config.js
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

Konfigurasi postcss.config.js biasanya sudah benar:

// postcss.config.js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Tambahkan direktif Tailwind ke file CSS utama. Biasanya ini adalah src/index.css. Hapus konten defaultnya dan tambahkan:

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Build CSS src/index.css menjadi file output css dengan perintah berikut:

npx tailwindcss -i ./src/index.css -o ./src/output.css --watch

Maka akan membuat file output.css di folder src.

Impor file CSS ini ke dalam src/main.jsx:

// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './output.css';
import { BrowserRouter as Router } from 'react-router-dom'; // Impor Router
import { AuthProvider } from './contexts/AuthContext.jsx'; // Impor AuthProvider

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Router> {/* Bungkus dengan Router */}
      <AuthProvider> {/* Bungkus dengan AuthProvider */}
        <App />
      </AuthProvider>
    </Router>
  </React.StrictMode>,
);

Install Axios dan React Router DOM:

Axios untuk HTTP request dan React Router DOM untuk routing.

npm install axios react-router-dom

Konfigurasi Instance Axios: Buat file src/lib/axios.js untuk konfigurasi global Axios.

// src/lib/axios.js
import axios from 'axios';

const apiClient = axios.create({
    baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000', // Ambil dari environment variable Vite
    withCredentials: true, // PENTING: Mengirim cookie session Sanctum
    withXSRFToken: true,
    headers: {
        'X-Requested-With': 'XMLHttpRequest', // Laravel mengenali ini sebagai request AJAX
        'Accept': 'application/json',
    }
});

// Fungsi untuk mendapatkan CSRF cookie sebelum request yang mengubah state (POST, PUT, DELETE, PATCH)
export const getCsrfCookie = async () => {
    try {
        await apiClient.get('/sanctum/csrf-cookie');
        console.log("CSRF cookie fetched");
    } catch (error) {
        console.error('Error fetching CSRF cookie:', error);
        // Mungkin perlu penanganan error lebih lanjut di sini
    }
};

export default apiClient;

Buat file .env.local di root proyek React (spa-react-frontend/.env.local) dan tambahkan:

// .env.local (di root proyek React)
VITE_API_BASE_URL=http://localhost:8000

Pastikan URL ini sesuai dengan URL backend Laravel. withCredentials: true sangat penting agar cookie session dari Laravel dapat dikirim dan diterima.

Manajemen State Autentikasi (React Context API):

Kita akan menggunakan Context API untuk manajemen state pengguna yang sederhana. Buat folder src/contexts dan di dalamnya file AuthContext.jsx:

// src/contexts/AuthContext.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import apiClient, { getCsrfCookie } from '../lib/axios';
import { useNavigate } from 'react-router-dom'; // Untuk redirect

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [isLoading, setIsLoading] = useState(true); // Untuk status loading awal
    const [errors, setErrors] = useState({}); // Untuk menyimpan error dari API
    const navigate = useNavigate();

    // Fungsi untuk mengambil data pengguna saat ini
    const fetchUser = async () => {
        setIsLoading(true);
        setErrors({});
        try {
            const response = await apiClient.get('/api/user');
            setUser(response.data);
        } catch (error) {
            if (error.response && error.response.status === 401) {
                setUser(null); // Tidak terautentikasi
            } else {
                console.error('Failed to fetch user:', error);
                setErrors({ general: "Gagal memuat data pengguna." });
            }
        } finally {
            setIsLoading(false);
        }
    };

    // Coba fetch user saat AuthProvider pertama kali dimuat
    useEffect(() => {
        fetchUser();
    }, []);

    const login = async (credentials) => {
        setIsLoading(true);
        setErrors({});
        await getCsrfCookie(); // Penting sebelum login
        try {
            const response = await apiClient.post('api/login', credentials);
            setUser(response.data);
            navigate('/dashboard'); // Arahkan ke dashboard setelah login
        } catch (error) {
            console.error("Login error:", error.response);
            if (error.response && error.response.data && error.response.data.errors) {
                setErrors(error.response.data.errors);
            } else if (error.response && error.response.data && error.response.data.message) {
                 setErrors({ form: error.response.data.message });
            } else {
                setErrors({ form: 'Login gagal. Periksa kembali email dan password.' });
            }
            throw error; // Dilempar agar bisa ditangkap di komponen
        } finally {
            setIsLoading(false);
        }
    };

    const register = async (data) => {
        setIsLoading(true);
        setErrors({});
        await getCsrfCookie(); // Penting sebelum register
        try {
            const response = await apiClient.post('api/register', data);
            setUser(response.data); // User langsung login
            navigate('/dashboard'); // Arahkan ke dashboard setelah registrasi
        } catch (error) {
            console.error("Register error:", error.response);
            if (error.response && error.response.data && error.response.data.errors) {
                setErrors(error.response.data.errors);
            } else if (error.response && error.response.data && error.response.data.message) {
                 setErrors({ form: error.response.data.message });
            } else {
                setErrors({ form: 'Registrasi gagal. Silakan coba lagi.' });
            }
            throw error; // Dilempar agar bisa ditangkap di komponen
        } finally {
            setIsLoading(false);
        }
    };

    const logout = async () => {
        setIsLoading(true);
        setErrors({});
        // Meskipun opsional untuk logout di beberapa kasus, CSRF cookie baik untuk konsistensi
        // dan jika ada aksi server-side yang memerlukan perlindungan CSRF saat logout.
        await getCsrfCookie();
        try {
            await apiClient.post('api/logout');
            setUser(null);
            navigate('/login'); // Arahkan ke login setelah logout
        } catch (error) {
            console.error('Logout error:', error);
            setErrors({ general: "Gagal logout." });
        } finally {
            setIsLoading(false);
        }
    };

    return (
        <AuthContext.Provider value={{ user, isLoading, login, register, logout, errors, setErrors, fetchUser }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => {
    const context = useContext(AuthContext);
    if (context === undefined) {
        throw new Error('useAuth must be used within an AuthProvider');
    }
    return context;
};

Bungkus aplikasi dengan AuthProvider di src/main.jsx. Dan juga perlu BrowserRouter dari react-router-dom.

// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './output.css';
import { BrowserRouter as Router } from 'react-router-dom'; // Impor Router
import { AuthProvider } from './contexts/AuthContext.jsx'; // Impor AuthProvider

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Router> {/* Bungkus dengan Router */}
      <AuthProvider> {/* Bungkus dengan AuthProvider */}
        <App />
      </AuthProvider>
    </Router>
  </React.StrictMode>,
);

Membuat Halaman dan Komponen :

Buat folder src/pages untuk halaman dan buat folder src/components untuk komponen. Untuk struktur halaman dan komponen yang akan kita buat seperti berikut:

src/
├── pages/
│   └── HomePage.jsx             ← Halaman utama sebelum login
│   └── LoginPage.jsx            ← halaman Login
│   └── RegisterPage.jsx         ← Halaman Registrasi
│   └── DashboardPage.jsx        ← Dahboard setelah login 
├── components/
│   ├── ProtectedRoute.jsx       ← Untuk Proteksi Route

Komponen ProtectedRoute.jsx : Komponen ini akan melindungi rute yang hanya bisa diakses oleh pengguna yang sudah login.

// src/components/ProtectedRoute.jsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

const ProtectedRoute = () => {
    const { user, isLoading } = useAuth();

    if (isLoading) {
        return <div className="flex justify-center items-center h-screen text-xl">Memuat...</div>;
    }

    return user ? <Outlet /> : <Navigate to="/login" replace />;
};

export default ProtectedRoute;

Halaman HomePage.jsx:

// src/pages/HomePage.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

const HomePage = () => {
    const { user, logout, isLoading } = useAuth();

    if (isLoading && !user) return <p className="text-center mt-10">Memuat...</p>;

    return (
        <div className="container mx-auto p-6">
            <header className="bg-indigo-600 text-white p-6 rounded-lg shadow-md mb-8">
                <h1 className="text-4xl font-bold text-center">Selamat Datang di Aplikasi Kami!</h1>
                <p className="text-center text-indigo-200 mt-2">Kelola akun dengan mudah.</p>
            </header>

            <nav className="flex justify-center space-x-6 mb-10">
                {!user && (
                    <>
                        <Link to="/login" className="px-6 py-3 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 transition duration-150">
                            Login
                        </Link>
                        <Link to="/register" className="px-6 py-3 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-600 transition duration-150">
                            Register
                        </Link>
                    </>
                )}
            </nav>

            {user && (
                <div className="text-center bg-white p-8 rounded-lg shadow-xl max-w-md mx-auto">
                    <h2 className="text-2xl font-semibold text-gray-800 mb-4">Anda Telah Login Sebagai:</h2>
                    <p className="text-xl text-gray-700 mb-2">{user.name}</p>
                    <p className="text-md text-gray-500 mb-6">{user.email}</p>
                    <div className="space-x-4">
                        <Link to="/dashboard" className="px-6 py-3 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-600 transition duration-150">
                            Dashboard
                        </Link>
                        <button
                            onClick={logout}
                            className="px-6 py-3 bg-red-500 text-white font-semibold rounded-lg shadow-md hover:bg-red-600 transition duration-150"
                        >
                            Logout
                        </button>
                    </div>
                </div>
            )}

            {!user && (
                <p className="text-center text-gray-600 mt-8">
                    Silakan login atau register untuk melanjutkan.
                </p>
            )}
        </div>
    );
};

export default HomePage;

Halaman LoginPage.jsx:

// src/pages/LoginPage.jsx
import React, { useState, useEffect }from 'react';
import { useAuth } from '../contexts/AuthContext';
import { Link, useNavigate } from 'react-router-dom';

const LoginPage = () => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const { login, errors, setErrors, isLoading: authLoading, user } = useAuth();
    const [isSubmitting, setIsSubmitting] = useState(false);
    const navigate = useNavigate();

    // Redirect jika sudah login
    useEffect(() => {
        if (user && !authLoading) {
            navigate('/dashboard', { replace: true });
        }
    }, [user, authLoading, navigate]);


    const handleSubmit = async (e) => {
        e.preventDefault();
        setIsSubmitting(true);
        setErrors({}); // Bersihkan error sebelumnya
        try {
            await login({ email, password });
            // Navigasi sudah dihandle di AuthContext atau useEffect di atas
        } catch (error) {
            // Error sudah dihandle dan disimpan di AuthContext (errors.form atau errors[field])
            console.error("Login attempt failed");
        } finally {
            setIsSubmitting(false);
        }
    };

    // Jika masih loading auth atau sudah ada user, jangan render form
    if (authLoading) return <div className="flex justify-center items-center h-screen">Memuat...</div>;
    if (user) return null; // Akan di-redirect oleh useEffect


    return (
        <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-indigo-500 to-purple-600 p-4">
            <div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md">
                <h2 className="text-3xl font-bold mb-8 text-center text-gray-800">Login Akun</h2>
                {errors.form && <p className="bg-red-100 text-red-700 p-3 rounded-md text-sm mb-4 text-center">{errors.form}</p>}
                <form onSubmit={handleSubmit}>
                    <div className="mb-5">
                        <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">Email</label>
                        <input
                            type="email"
                            id="email"
                            value={email}
                            onChange={(e) => setEmail(e.target.value)}
                            required
                            className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                            placeholder="email@email.com"
                        />
                        {errors.email && <p className="text-red-500 text-xs mt-1">{errors.email[0]}</p>}
                    </div>
                    <div className="mb-7">
                        <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">Password</label>
                        <input
                            type="password"
                            id="password"
                            value={password}
                            onChange={(e) => setPassword(e.target.value)}
                            required
                            className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                            placeholder="********"
                        />
                        {errors.password && <p className="text-red-500 text-xs mt-1">{errors.password[0]}</p>}
                    </div>
                    <button
                        type="submit"
                        disabled={isSubmitting || authLoading}
                        className="w-full bg-indigo-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-60 transition duration-150"
                    >
                        {isSubmitting || authLoading ? 'Memproses...' : 'Login'}
                    </button>
                </form>
                <p className="mt-8 text-center text-sm text-gray-600">
                    Belum punya akun? <Link to="/register" className="font-medium text-indigo-600 hover:text-indigo-500 hover:underline">Daftar di sini</Link>
                </p>
            </div>
        </div>
    );
};

export default LoginPage;

Halaman RegisterPage.jsx:

// src/pages/RegisterPage.jsx
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { Link, useNavigate } from 'react-router-dom';

const RegisterPage = () => {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [passwordConfirmation, setPasswordConfirmation] = useState('');
    const { register, errors, setErrors, isLoading: authLoading, user } = useAuth();
    const [isSubmitting, setIsSubmitting] = useState(false);
    const navigate = useNavigate();

     // Redirect jika sudah login
    useEffect(() => {
        if (user && !authLoading) {
            navigate('/dashboard', { replace: true });
        }
    }, [user, authLoading, navigate]);


    const handleSubmit = async (e) => {
        e.preventDefault();
        setIsSubmitting(true);
        setErrors({}); // Bersihkan error sebelumnya

        if (password !== passwordConfirmation) {
            setErrors({ password_confirmation: ["Konfirmasi password tidak cocok."] });
            setIsSubmitting(false);
            return;
        }

        try {
            await register({ name, email, password, password_confirmation: passwordConfirmation });
            // Navigasi sudah dihandle di AuthContext atau useEffect di atas
        } catch (error) {
            // Error sudah dihandle dan disimpan di AuthContext
            console.error("Register attempt failed");
        } finally {
            setIsSubmitting(false);
        }
    };

    if (authLoading) return <div className="flex justify-center items-center h-screen">Memuat...</div>;
    if (user) return null;


    return (
        <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-green-400 to-teal-500 p-4">
            <div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md">
                <h2 className="text-3xl font-bold mb-8 text-center text-gray-800">Buat Akun Baru</h2>
                {errors.form && <p className="bg-red-100 text-red-700 p-3 rounded-md text-sm mb-4 text-center">{errors.form}</p>}
                <form onSubmit={handleSubmit}>
                    <div className="mb-5">
                        <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">Nama Lengkap</label>
                        <input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)} required className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm" placeholder="Nama Anda" />
                        {errors.name && <p className="text-red-500 text-xs mt-1">{errors.name[0]}</p>}
                    </div>
                    <div className="mb-5">
                        <label htmlFor="email-register" className="block text-sm font-medium text-gray-700 mb-1">Email</label>
                        <input type="email" id="email-register" value={email} onChange={(e) => setEmail(e.target.value)} required className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm" placeholder="anda@email.com" />
                        {errors.email && <p className="text-red-500 text-xs mt-1">{errors.email[0]}</p>}
                    </div>
                    <div className="mb-5">
                        <label htmlFor="password-register" className="block text-sm font-medium text-gray-700 mb-1">Password</label>
                        <input type="password" id="password-register" value={password} onChange={(e) => setPassword(e.target.value)} required className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm" placeholder="Minimal 8 karakter" />
                        {errors.password && <p className="text-red-500 text-xs mt-1">{errors.password[0]}</p>}
                    </div>
                    <div className="mb-7">
                        <label htmlFor="password_confirmation" className="block text-sm font-medium text-gray-700 mb-1">Konfirmasi Password</label>
                        <input type="password" id="password_confirmation" value={passwordConfirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} required className="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm" placeholder="Ulangi password" />
                        {errors.password_confirmation && <p className="text-red-500 text-xs mt-1">{errors.password_confirmation[0]}</p>}
                    </div>
                    <button type="submit" disabled={isSubmitting || authLoading} className="w-full bg-green-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-60 transition duration-150">
                        {isSubmitting || authLoading ? 'Memproses...' : 'Daftar'}
                    </button>
                </form>
                <p className="mt-8 text-center text-sm text-gray-600">
                    Sudah punya akun? <Link to="/login" className="font-medium text-green-600 hover:text-green-500 hover:underline">Login di sini</Link>
                </p>
            </div>
        </div>
    );
};

export default RegisterPage;

Halaman DashboardPage.jsx (Contoh Halaman Terproteksi):

// src/pages/DashboardPage.jsx
import React from 'react';
import { useAuth } from '../contexts/AuthContext';

const DashboardPage = () => {
    const { user, logout, isLoading } = useAuth();

    if (isLoading) {
        return <div className="flex justify-center items-center h-screen text-xl">Memuat dashboard...</div>;
    }

    if (!user) {
        // Seharusnya tidak terjadi jika ProtectedRoute bekerja, tapi sebagai fallback.
        return <p className="text-center mt-10 text-red-500">Anda tidak diizinkan mengakses halaman ini.</p>;
    }

    return (
        <div className="container mx-auto p-6 min-h-screen bg-gray-50">
            <div className="bg-white shadow-xl rounded-lg p-8 max-w-3xl mx-auto mt-10">
                <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 pb-4 border-b border-gray-200">
                    <div>
                        <h1 className="text-4xl font-bold text-indigo-700">Dashboard Pengguna</h1>
                        <p className="text-gray-500 mt-1">Selamat datang kembali di area pribadi Anda.</p>
                    </div>
                    <button
                        onClick={logout}
                        className="mt-4 sm:mt-0 bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out"
                    >
                        Logout
                    </button>
                </div>

                <div className="bg-indigo-50 p-6 rounded-lg border border-indigo-200">
                    <h2 className="text-2xl font-semibold text-gray-800 mb-3">Informasi Akun:</h2>
                    <p className="text-lg text-gray-700 mb-1"><span className="font-medium">Nama:</span> {user.name}</p>
                    <p className="text-lg text-gray-700"><span className="font-medium">Email:</span> {user.email}</p>
                    {/* Tampilkan data pengguna lainnya atau konten dashboard di sini */}
                </div>

                <div className="mt-8">
                    <h3 className="text-xl font-semibold text-gray-700 mb-3">Aksi Cepat:</h3>
                    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                        <button className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-lg shadow transition duration-150">
                            Edit Profil (Contoh)
                        </button>
                        <button className="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg shadow transition duration-150">
                            Lihat Aktivitas (Contoh)
                        </button>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default DashboardPage;

Konfigurasi Rute Utama di App.jsx:

// src/App.jsx
import React from 'react';
import { Routes, Route, Navigate, Link } from 'react-router-dom';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import DashboardPage from './pages/DashboardPage';
import ProtectedRoute from './components/ProtectedRoute';
import { useAuth } from './contexts/AuthContext';

// Komponen untuk rute yang hanya bisa diakses jika BELUM login
const GuestRoute = ({ children }) => {
    const { user, isLoading } = useAuth();
    if (isLoading) return <div className="flex justify-center items-center h-screen">Memuat...</div>;
    return !user ? children : <Navigate to="/dashboard" replace />;
};

function App() {
  return (
    
    <>
      <Routes>
        <Route path="/" element={<HomePage />} />

        {/* Rute untuk tamu (belum login) */}
        <Route path="/login" element={<GuestRoute><LoginPage /></GuestRoute>} />
        <Route path="/register" element={<GuestRoute><RegisterPage /></GuestRoute>} />

        {/* Rute Terproteksi (sudah login) */}
        <Route element={<ProtectedRoute />}>
          <Route path="/dashboard" element={<DashboardPage />} />
          {/* Tambahkan rute terproteksi lainnya di sini */}
          {/* <Route path="/profile" element={<ProfilePage />} /> */}
        </Route>

        {/* Rute fallback atau halaman 404 */}
        <Route path="*" element={
            <div className="text-center py-20">
                <h1 className="text-4xl font-bold mb-4">404 - Halaman Tidak Ditemukan</h1>
                <Link to="/" className="text-indigo-600 hover:underline">Kembali ke Beranda</Link>
            </div>
        } />
      </Routes>
    </>
  );
}

export default App;

 

Bagian 3: Testing Aplikasi 

Jalankan Server Development Laravel: Di terminal, navigasi ke direktori backend (spa-laravel-backend):

php artisan serve

Secara default, ini akan berjalan di http://localhost:8000.

Jalankan Server Development React: Di terminal lain, navigasi ke direktori frontend Anda (spa-react-frontend):

npm run dev

Secara default, ini akan berjalan di http://localhost:5173 (atau port lain jika 5173 sudah digunakan).

Lakukan testing registrasi, dan kemudian login.

Halaman registrasi di http://localhost:5173/register. jika berhasil, akan seperti berikut:

registrasi SPA Laravel react

Dan juga login di URLhttp://localhost:5173/login. Jika berhasil akan seperti berikut:
Login SPA Laravel React

Ketika login berhasil, akan didirect ke halaman Dashboard seperti berikut:

Dashboard SPA Laravel React

Selesai!!! 
Selamat mencoba. Silahkan komen jika ada yang perlu didiskusikan :)