# Inertia.js Laravel Adapter Inertia.js is a modern approach to building single-page applications (SPAs) without the complexity of a separate API. The Laravel adapter allows you to create fully client-side rendered, single-page apps using classic server-side routing and controllers. Instead of returning JSON responses, Inertia enables Laravel controllers to return JavaScript page components (Vue, React, or Svelte) that receive data as props, eliminating the need for a separate API layer while maintaining the benefits of both server-side and client-side development. This adapter provides the server-side implementation for Laravel applications, handling the protocol between Laravel backend and JavaScript frontend frameworks. It manages page rendering, shared data, partial reloads, deferred props, history encryption, server-side rendering (SSR), and seamless navigation between pages. The library integrates deeply with Laravel's routing, middleware, validation, and testing systems. ## Rendering Inertia Pages The `Inertia::render()` method creates responses that render JavaScript components with data passed as props. This is the primary method for returning Inertia responses from controllers. ```php use Inertia\Inertia; // Basic rendering with props class UserController extends Controller { public function index() { return Inertia::render('Users/Index', [ 'users' => User::all(), 'filters' => request()->only(['search', 'status']), ]); } public function show(User $user) { return Inertia::render('Users/Show', [ 'user' => $user->load('posts', 'comments'), 'canEdit' => auth()->user()->can('update', $user), ]); } } // Using the helper function public function create() { return inertia('Users/Create', [ 'roles' => Role::all(), ]); } // Adding props fluently with the with() method public function edit(User $user) { return Inertia::render('Users/Edit') ->with('user', $user) ->with(['roles' => Role::all(), 'departments' => Department::all()]); } ``` ## Sharing Global Data The `Inertia::share()` method makes data available to all Inertia responses. Commonly used for authentication state, flash messages, and application-wide settings. ```php // In app/Http/Middleware/HandleInertiaRequests.php class HandleInertiaRequests extends Middleware { public function share(Request $request): array { return [ ...parent::share($request), 'auth' => [ 'user' => $request->user() ? [ 'id' => $request->user()->id, 'name' => $request->user()->name, 'email' => $request->user()->email, ] : null, ], 'flash' => [ 'success' => session('success'), 'error' => session('error'), ], 'appName' => config('app.name'), ]; } } // Share data dynamically in a service provider public function boot() { Inertia::share('appVersion', fn () => config('app.version')); Inertia::share([ 'locale' => fn () => app()->getLocale(), 'timezone' => config('app.timezone'), ]); } // Retrieve shared data $allShared = Inertia::getShared(); $authData = Inertia::getShared('auth'); $defaultValue = Inertia::getShared('missing.key', 'default'); ``` ## Deferred Props Deferred props are loaded asynchronously after the initial page render, improving perceived performance by showing the page immediately while slower data loads in the background. ```php use Inertia\Inertia; class DashboardController extends Controller { public function index() { return Inertia::render('Dashboard', [ // Fast data loads immediately 'user' => auth()->user(), 'notifications' => auth()->user()->unreadNotifications->take(5), // Slow data loads after page render (default group) 'analytics' => Inertia::defer(fn () => AnalyticsService::getStats()), // Group related deferred props to load together 'recentOrders' => Inertia::defer( fn () => Order::recent()->with('items')->get(), 'activity' ), 'recentComments' => Inertia::defer( fn () => Comment::recent()->with('user')->get(), 'activity' ), // Heavy report loads in its own group 'salesReport' => Inertia::defer( fn () => ReportService::generateSalesReport(), 'reports' ), ]); } } ``` ## Optional Props Optional props are only evaluated when explicitly requested via partial reloads, useful for expensive data that isn't always needed. ```php use Inertia\Inertia; class ProductController extends Controller { public function show(Product $product) { return Inertia::render('Products/Show', [ 'product' => $product, 'reviews' => $product->reviews()->paginate(10), // Only loaded when explicitly requested 'relatedProducts' => Inertia::optional( fn () => ProductService::findRelated($product, limit: 12) ), 'priceHistory' => Inertia::optional( fn () => $product->priceHistory()->orderByDesc('date')->get() ), 'inventoryLevels' => Inertia::optional( fn () => WarehouseService::getInventory($product) ), ]); } } // Frontend can request optional props via partial reload: // router.reload({ only: ['relatedProducts', 'priceHistory'] }) ``` ## Always Props Always props are included in every response, even during partial reloads. Essential for data that must always be current like validation errors or CSRF tokens. ```php use Inertia\Inertia; class HandleInertiaRequests extends Middleware { public function share(Request $request): array { return [ // Always included even in partial reloads 'errors' => Inertia::always(fn () => $this->resolveValidationErrors($request)), 'csrf_token' => Inertia::always(fn () => csrf_token()), // Regular shared props (excluded during partial reloads unless requested) 'auth' => fn () => [ 'user' => $request->user(), ], ]; } } // In controller - always include current permissions public function index() { return Inertia::render('Posts/Index', [ 'posts' => Post::paginate(), 'permissions' => Inertia::always(fn () => auth()->user()->permissions), ]); } ``` ## Merge Props Merge props combine new data with existing client-side state during partial reloads, enabling infinite scroll and pagination without replacing the entire dataset. ```php use Inertia\Inertia; class PostController extends Controller { public function index() { $posts = Post::query() ->orderByDesc('created_at') ->paginate(15); return Inertia::render('Posts/Index', [ // Append new pages to existing posts array 'posts' => Inertia::merge($posts), ]); } } // Deep merge for nested objects public function dashboard() { return Inertia::render('Dashboard', [ // Deep merge preserves nested structure 'settings' => Inertia::deepMerge([ 'notifications' => NotificationSettings::forUser(auth()->user()), 'privacy' => PrivacySettings::forUser(auth()->user()), ]), ]); } ``` ## Scroll Props for Infinite Scrolling Scroll props provide specialized merge behavior with pagination metadata for implementing infinite scroll functionality. ```php use Inertia\Inertia; class FeedController extends Controller { public function index() { $posts = Post::query() ->with('author', 'comments') ->orderByDesc('created_at') ->cursorPaginate(20); return Inertia::render('Feed/Index', [ // Scroll prop with automatic pagination metadata 'posts' => Inertia::scroll($posts) ->configureMergeIntent() // Reads merge direction from request header ->append('data'), // Append new items to 'data' wrapper ]); } } // Custom scroll metadata public function timeline() { $events = Event::paginate(25); return Inertia::render('Timeline', [ 'events' => Inertia::scroll( $events, wrapper: 'items', metadata: fn ($paginator) => new class($paginator) implements ProvidesScrollMetadata { public function __construct(private $paginator) {} public function getPageName(): string { return 'page'; } public function getCurrentPage(): int { return $this->paginator->currentPage(); } public function getNextPage(): ?int { return $this->paginator->hasMorePages() ? $this->paginator->currentPage() + 1 : null; } public function getPreviousPage(): ?int { return $this->paginator->currentPage() > 1 ? $this->paginator->currentPage() - 1 : null; } } ), ]); } ``` ## Once Props Once props are evaluated only on the first request and remembered by the client across subsequent navigations, useful for expensive data that rarely changes. ```php use Inertia\Inertia; class HandleInertiaRequests extends Middleware { public function shareOnce(Request $request): array { return [ // Loaded once and remembered across navigations 'permissions' => fn () => PermissionService::forUser($request->user()), 'featureFlags' => fn () => FeatureFlag::all(), ]; } } // In controller public function index() { return Inertia::render('Settings', [ 'settings' => UserSettings::forUser(auth()->user()), // Expensive lookup done only once 'availableTimezones' => Inertia::once(fn () => timezone_identifiers_list()), 'countries' => Inertia::once(fn () => Country::with('states')->get()), ]); } ``` ## Flash Data Flash data is included with the response but not persisted in browser history state, ideal for toast notifications and one-time messages. ```php use Inertia\Inertia; class OrderController extends Controller { public function store(Request $request) { $order = Order::create($request->validated()); // Flash data for one-time display Inertia::flash('toast', [ 'type' => 'success', 'message' => 'Order #' . $order->id . ' created successfully!', ]); return redirect()->route('orders.show', $order); } public function destroy(Order $order) { $order->delete(); // Multiple flash values Inertia::flash('notification', 'Order deleted') ->flash('highlight', 'orders-table'); return redirect()->route('orders.index'); } } // Flash directly on response public function update(Request $request, Post $post) { $post->update($request->validated()); return Inertia::render('Posts/Show', ['post' => $post]) ->flash('saved', true) ->flash('message', 'Post updated successfully'); } ``` ## External Redirects The `Inertia::location()` method triggers a full page visit to external URLs or forces a server-side redirect when needed. ```php use Inertia\Inertia; class PaymentController extends Controller { public function checkout() { $checkoutUrl = PaymentGateway::createSession([ 'amount' => Cart::total(), 'success_url' => route('payment.success'), 'cancel_url' => route('payment.cancel'), ]); // Full page redirect to external payment gateway return Inertia::location($checkoutUrl); } public function oauth(string $provider) { // Redirect to OAuth provider return Inertia::location( Socialite::driver($provider)->redirect() ); } } // Using the helper function public function externalLink() { return inertia_location('https://external-service.com/callback'); } ``` ## History Encryption History encryption prevents sensitive data from being accessible in the browser's history state after logout, enhancing security for sensitive applications. ```php // config/inertia.php return [ 'history' => [ 'encrypt' => env('INERTIA_ENCRYPT_HISTORY', true), ], ]; // Enable per-request in middleware class HandleInertiaRequests extends Middleware { public function rootView(Request $request): string { // Encrypt history for authenticated users if ($request->user()) { Inertia::encryptHistory(); } return 'app'; } } // Enable via route middleware Route::middleware(['auth', 'inertia.encrypt'])->group(function () { Route::get('/account', [AccountController::class, 'index']); Route::get('/billing', [BillingController::class, 'index']); }); // Clear history on logout public function logout(Request $request) { Auth::logout(); Inertia::clearHistory(); return redirect()->route('login'); } ``` ## Route Macros The `Route::inertia()` macro provides a shorthand for simple pages that don't need controller logic. ```php use Illuminate\Support\Facades\Route; // Simple static pages Route::inertia('/', 'Home'); Route::inertia('/about', 'About'); Route::inertia('/contact', 'Contact'); // With props Route::inertia('/pricing', 'Pricing', [ 'plans' => fn () => Plan::active()->get(), ]); // With middleware Route::middleware(['auth'])->group(function () { Route::inertia('/dashboard', 'Dashboard', [ 'stats' => fn () => DashboardStats::forUser(auth()->user()), ]); }); // Named routes Route::inertia('/terms', 'Legal/Terms')->name('terms'); Route::inertia('/privacy', 'Legal/Privacy')->name('privacy'); ``` ## Custom Middleware Create custom Inertia middleware to customize shared data, versioning, and root view behavior. ```php // Generate middleware: php artisan inertia:middleware namespace App\Http\Middleware; use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { protected $rootView = 'app'; public function version(Request $request): ?string { // Custom versioning based on deployment return config('app.version') . '-' . config('app.asset_version'); } public function share(Request $request): array { return [ ...parent::share($request), 'auth' => [ 'user' => $request->user()?->only('id', 'name', 'email', 'avatar'), 'permissions' => $request->user()?->permissions ?? [], ], 'flash' => [ 'success' => session('success'), 'error' => session('error'), 'warning' => session('warning'), ], 'app' => [ 'name' => config('app.name'), 'locale' => app()->getLocale(), 'supportedLocales' => config('app.supported_locales'), ], ]; } public function rootView(Request $request): string { // Different layout for admin routes if ($request->routeIs('admin.*')) { return 'admin'; } return $this->rootView; } } // Register in bootstrap/app.php (Laravel 11+) or Kernel.php ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); }) ``` ## Testing Inertia Responses The testing utilities provide fluent assertions for verifying Inertia responses in feature tests. ```php use Inertia\Testing\AssertableInertia; class UserControllerTest extends TestCase { public function test_users_index_displays_users() { $users = User::factory()->count(3)->create(); $response = $this->actingAs($users->first()) ->get('/users'); $response->assertInertia(fn (AssertableInertia $page) => $page ->component('Users/Index') ->has('users', 3) ->has('users.0', fn ($user) => $user ->has('id') ->has('name') ->has('email') ->missing('password') ) ); } public function test_user_show_page() { $user = User::factory()->create(['name' => 'John Doe']); $this->get("/users/{$user->id}") ->assertInertia(fn (AssertableInertia $page) => $page ->component('Users/Show') ->where('user.name', 'John Doe') ->where('user.id', $user->id) ); } public function test_partial_reload() { $user = User::factory()->create(); $this->get('/dashboard') ->assertInertia(fn (AssertableInertia $page) => $page ->component('Dashboard') ->has('stats') // Test deferred props loading ->loadDeferredProps('activity', fn ($page) => $page ->has('recentOrders') ->has('recentComments') ) // Test partial reload ->reloadOnly('notifications', fn ($page) => $page ->has('notifications') ->missing('stats') ) ); } public function test_flash_data() { $this->post('/orders', ['product_id' => 1]) ->assertRedirect('/orders/1') ->assertInertiaFlash('toast.type', 'success') ->assertInertiaFlash('toast.message'); } public function test_get_props_directly() { $response = $this->get('/users'); $props = $response->inertiaProps(); $users = $response->inertiaProps('users'); $page = $response->inertiaPage(); $this->assertCount(10, $users); $this->assertEquals('Users/Index', $page['component']); } } ``` ## Server-Side Rendering Configuration Configure server-side rendering for improved SEO and initial page load performance. ```php // config/inertia.php return [ 'ssr' => [ 'enabled' => env('INERTIA_SSR_ENABLED', true), 'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'), 'ensure_bundle_exists' => true, 'throw_on_error' => env('INERTIA_SSR_THROW_ON_ERROR', false), ], ]; // Exclude paths from SSR (e.g., heavy dashboards) class HandleInertiaRequests extends Middleware { protected $withoutSsr = [ '/admin/*', '/dashboard/analytics', ]; } // Or dynamically exclude paths Inertia::withoutSsr(['/reports/*', '/exports/*']); ``` ```bash # Start SSR server php artisan inertia:start-ssr # Stop SSR server php artisan inertia:stop-ssr # Check SSR server health php artisan inertia:check-ssr ``` ## Blade Directives The `@inertia` and `@inertiaHead` directives render the Inertia root element and SSR head content in your Blade templates. ```blade {{-- resources/views/app.blade.php --}}