Sharing Data Across Inertia Pages

+15 Mana ✨

Introduction

Every Inertia page needs access to a few common values — the authenticated user, flash messages, feature flags, CSRF tokens. Rather than passing them manually from every controller, Inertia provides a middleware where you declare them once and they're merged into every page's props automatically.

Key Concepts

  • Shared data: Values merged into every Inertia response's props, available to every page component without explicit passing.
  • HandleInertiaRequests: The middleware (installed at app/Http/Middleware/HandleInertiaRequests.php) where you declare shared values via its share() method.
  • Lazy evaluation: Shared values wrapped in a closure that only runs when the client explicitly requests them, useful for expensive lookups.
  • usePage(): A Vue/React hook that gives page components access to the current props including shared data.

Real World Context

Without shared data, every controller would need to manually pass auth.user, flash.success, and feature-flag state into every Inertia::render() call — 50 controllers means 50 copies of the same boilerplate. Shared data centralizes that logic so controllers focus on the page-specific props.

Deep Dive

Declaring Shared Data

Open app/Http/Middleware/HandleInertiaRequests.php and override share():

php
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => fn () => $request->user()?->only(['id', 'name', 'email']),
            ],
            'flash' => [
                'success' => fn () => $request->session()->get('success'),
                'error' => fn () => $request->session()->get('error'),
            ],
            'features' => [
                'billing_enabled' => fn () => config('features.billing'),
            ],
        ]);
    }
}

Notice every value is wrapped in a closure. Inertia only calls the closure when the request actually needs the value — on a partial reload that asks for posts only, the auth, flash, and features closures are skipped entirely.

Using Shared Data in a Component

vue
<script setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()
const user = computed(() => page.props.auth.user)
const flash = computed(() => page.props.flash)
</script>

<template>
    <header>
        <span v-if="user">Logged in as {{ user.name }}</span>
    </header>

    <div v-if="flash.success" class="alert alert-success">
        {{ flash.success }}
    </div>

    <slot />
</template>

Wrapping page.props accesses in computed() keeps the layout reactive — when Inertia updates the props after a navigation, the UI re-renders automatically.

Sharing Only What the User Can See

Be careful with shared data: everything in share() is serialized into the page's initial payload, including the HTML response. Never leak sensitive fields:

php
// ❌ Leaks every user attribute including password hash, tokens, etc.
'user' => fn () => $request->user(),

// ✅ Explicit allow-list
'user' => fn () => $request->user()?->only(['id', 'name', 'email']),

The only() call acts as a safe whitelist. An API resource (UserResource) is even better for formal projects.

Feature Flags and Environment

Shared data is a great home for environment-specific toggles:

php
'app' => [
    'env' => app()->environment(),
    'url' => config('app.url'),
],
'features' => [
    'new_dashboard' => fn () => auth()->user()?->hasFeature('new_dashboard'),
],

In the frontend you can then gate UI:

vue
<nav v-if="page.props.features.new_dashboard">
    <Link href="/dashboard/v2">New dashboard</Link>
</nav>

Common Pitfalls

  1. Sharing the whole user model — Sends password hashes, tokens, and any hidden fields in the HTML payload. Always use only() or an API resource.
  2. Forgetting to wrap expensive lookups in a closure — Non-closure values run on every request, including partial reloads that don't need them.
  3. Mutating shared data after share() — Shared data is resolved at response time. Any changes made later in the request lifecycle won't make it into the payload.

Best Practices

  1. Allow-list every user field — ->only(['id', 'name', 'email']) is safer than trusting $hidden.
  2. Use closures for optional or expensive values — Only computed when needed, cheaper on partial reloads.
  3. Keep shared data small — Everything in share() ships on every page. If a value is only used on one page, pass it from that page's controller instead.

Summary

  • HandleInertiaRequests::share() merges values into every Inertia response's props.
  • Wrap values in closures so they only resolve when needed — especially on partial reloads.
  • Sanitize user data with only() or API resources before sharing.
  • Access shared data via usePage().props in components, typically wrapped in a computed() for reactivity.
✓ Completed