Currently Available: Need a skilled Software Developer for your next project?
Categories
Inertia.js Laravel

Mastering Inertia.js Persistent Layouts

When building SaaS applications with Laravel and Inertia.js, you'll typically start with a nested layout approach. But there's a more powerful pattern available: persistent layouts. Let me show you the difference through a practical example.

Traditional Nested Layout Approach

In the traditional approach, every page component imports and wraps itself in a layout component:

<!-- Traditional Page Component (Dashboard.vue) -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import DashboardStats from '@/Components/DashboardStats.vue';
import { ref } from 'vue';

// Local state that will be reset on every navigation
const isSidebarOpen = ref(true);
</script>

<template>
  <AppLayout>
    <template #header>
      <h2 class="font-semibold text-xl">Dashboard</h2>
    </template>

    <div>
      <button @click="isSidebarOpen = !isSidebarOpen">
        Toggle Sidebar
      </button>
      <DashboardStats />
    </div>
  </AppLayout>
</template>

Persistent Layout Approach

With persistent layouts, your page component becomes simpler and cleaner:

<!-- Persistent Layout Page Component (Dashboard.vue) -->
<script setup>
import DashboardStats from '@/Components/DashboardStats.vue';
</script>

<template>
  <div>
    <DashboardStats />
  </div>
</template>

That's it! When using persistent layouts with a default layout configured in app.js, you don't even need to specify the layout in each page component.

Overriding the Default Layout

For pages that need a different layout than the default one:

<!-- Admin page with custom layout -->
<script setup>
import AdminLayout from '@/Layouts/AdminLayout.vue';
import { defineOptions } from 'vue';

// Override the default layout
defineOptions({
  layout: AdminLayout
});
</script>

<template>
  <div>
    <h1>Admin Dashboard</h1>
    <!-- Admin page content -->
  </div>
</template>

Pages Without a Layout

For pages like login screens that shouldn't use any layout:

<!-- Login page with no layout -->
<script setup>
import { defineOptions } from 'vue';

// Set layout to null to avoid using any layout
defineOptions({
  layout: null
});
</script>

<template>
  <div class="min-h-screen flex items-center justify-center bg-gray-100">
    <div class="w-full max-w-md">
      <h1 class="text-center text-2xl mb-6">Login</h1>
      <form class="bg-white rounded-lg shadow-md p-8">
        <!-- Login form fields -->
      </form>
    </div>
  </div>
</template>

The key differences:

  1. No layout wrapping - The page doesn't wrap itself in a layout component
  2. No layout import needed - When using a default layout in app.js
  3. Cleaner template code - No nested templates or named slots
  4. State preservation - Any state in the layout component (like sidebar toggle state) persists between page navigations

How Persistent Layouts Work

Normally, when you navigate between pages in an Inertia app, the entire page component tree (including your layout) is torn down and rebuilt. Persistent layouts change this.

With persistent layouts:

  1. State Preservation: Your main layout component (e.g., MainLayout.vue) doesn't get destroyed and recreated on navigation. This means any local state within that layout (like a sidebar's open/closed status, component state, or even the scroll position of the layout itself) is preserved automatically.
  2. Efficiency: Since the layout isn't constantly being rerendered, only the page-specific content needs to be swapped out, potentially improving performance.
  3. Cleaner Page Components: Your individual page components (like Dashboard.vue or UserProfile.vue) become simpler, as they no longer need to explicitly import and wrap themselves in the layout.

Implementation Steps

Let's look at how to implement persistent layouts in your Inertia.js application:

1. Update Inertia Configuration

The magic starts in your main JavaScript entry point (resources/js/app.js) within the createInertiaApp setup:

// resources/js/app.js
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import MainLayout from './Layouts/MainLayout.vue'; // Import your default layout

const appName = import.meta.env.VITE_APP_NAME || 'Laravel App';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,

    resolve: async (name) => {
        const page = await resolvePageComponent(
            `./Pages/${name}.vue`,
            import.meta.glob('./Pages/**/*.vue')
        );

        // Assign MainLayout as the default if the page doesn't specify its own
        page.default.layout = page.default.layout || MainLayout;

        return page;
    },

    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el);
    },
});

This configuration ensures that:

  • MainLayout.vue is used as the default layout for all pages
  • Pages can still specify a different layout if needed
  • The layout is preserved between page navigations

2. Create Your Layout Component

Your layout component will now use the default slot to render the current page:

<!-- resources/js/Layouts/MainLayout.vue -->
<script setup>
import { computed } from "vue";
import { usePage } from "@inertiajs/vue3";
import Navigation from "@/Components/Navigation.vue";

// IMPORTANT: Use computed properties for reactivity with page props
const page = usePage();
const title = computed(() => page.props.title || 'Default Title');
const user = computed(() => page.props.auth.user);
</script>

<template>
  <div class="min-h-screen bg-gray-100">
    <Navigation :user="user" />

    <header class="bg-white shadow">
      <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
        <h1 class="text-2xl font-semibold text-gray-900">{{ title }}</h1>
      </div>
    </header>

    <main>
      <div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <!-- The page component is rendered in this slot -->
        <slot />
      </div>
    </main>
  </div>
</template>

3. Handling Layout Data with Computed Properties

The most important consideration with persistent layouts is how shared data is handled. Since the layout doesn't remount on navigation, you must use Vue's computed properties to ensure your layout stays in sync with the latest data:

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

// This is the correct way
const user = computed(() => usePage().props.auth.user);
const notifications = computed(() => usePage().props.notifications);

// DON'T do this - it won't update on navigation
// const user = ref(usePage().props.auth.user);
</script>

Computed properties create a reactive connection to Inertia's page props, ensuring that your layout always displays the most current data.

4. Converting Existing Pages

To convert your existing pages to use persistent layouts:

  1. Remove the layout import and wrapper tags
  2. Remove any named slot templates
  3. Ensure your controllers pass any layout-specific data as props
  4. Optionally specify a different layout or set to null for no layout

Before:

<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
</script>

<template>
  <AppLayout>
    <template #header>
      <h2>User Profile</h2>
    </template>
    <div>Profile content here</div>
  </AppLayout>
</template>

After:

<script setup>
// No layout import needed
</script>

<template>
  <div>Profile content here</div>
</template>

And in your controller:

// Controller passes title to be used by layout
return Inertia::render('Profile/Show', [
    'title' => 'User Profile',
    // other data...
]);

Benefits for SaaS Applications

Persistent layouts offer several significant benefits for SaaS applications:

  1. Improved Performance: Only the changing content is updated, not the entire layout
  2. State Preservation: Navigation state like sidebar toggles, active tabs, and scroll position is maintained
  3. Reduced API Calls: API calls in layout components only happen once instead of on every page load
  4. Smoother UX: No flashing or rerendering of navigation and UI chrome during page transitions

These benefits are especially valuable for data-heavy SaaS applications where users frequently navigate between different sections.

Handling Potential Drawbacks

There are a few considerations to keep in mind:

  1. Data Staleness: Since layouts don't remount, you need to ensure data stays fresh using computed properties
  2. Memory Usage: Persistent components consume memory as they remain mounted
  3. Debugging Complexity: The component lifecycle behaves differently than in traditional Vue applications

The computed property pattern shown earlier is important for avoiding stale data issues.

Be Careful

Implementing persistent layouts in your Inertia.js application can significantly improve both performance and user experience. By following the steps outlined in this post, you can transform your application to take full advantage of this powerful pattern.

The initial setup requires some careful thinking about data flow, but the benefits in terms of smoother navigation and state preservation make it well worth the effort for sophisticated SaaS applications.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Subscribe to my newsletter

Get new posts when I publish them.

I respect your privacy. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *