Skip to content

Inertia: Partial Initial Pages

Use Case

In case you didn't already know, Inertia JS makes building a Vue app with Laravel a total breeze.

However, there's been on feature than I've run into limitations with; partial reloads. Particularly, the fact that partial reloads only work for visits made to the same page component.

This might not be an issue for most, but I found myself wishing I could load only some of the page props on an initial page load. The reason this would be helpful is that the project I was working on was the intranet site for the company I was working for; it was a large application with multiple "apps" within it serving as tools for various departments within the company.

The intranet Laravel application was originally written in 100% Blade pages, and I was tasked getting started with rebuilding everything in Vue. I found myself utilizing Pinia to store persistent app-specific data for multi-page sub-apps. With this design structure I could load the data I needed to bootstrap the app in the first request, and then omit them in subsequent requests.

The only problem was that this design pattern wasn't really supported with Inertia unless the entire sub-app was contained within a single page component. If the sub-app had multiple pages, the end user should be able to initiate a page load from any page, load the data needed to bootstrap the Pinia store, and then have all subsequent page requests omit any unnecessary page props to speed everything up.

For a little while, I got around this by adding a wrapper component that was shared between all of a given sub-app's pages which itself checked in the Pinia store was instantiated and make additional API calls to fetch the necessary Pinia store data if not. However, this was not ideal for the following reasons:

  • A given page will finish loading irrespective of whether or not the Pinia store, which the page relies on, is ready. This forced me to have to use placeholder skeletons which can cause frantic rending & rerendering of the page for the end user, especially if the bootstrapping data is fetched quickly.
  • It results in another HTTP request fired off immediately after the page loads on the initial visit; it sounds trivial, but in practice the overhead of fetching the data I wanted with an additional API call caused me to have to setup more boilerplate code and added an unnatural flow to both the UX and DX that I just wasn't satisfied with.

I kept thinking it would be a lot simpler if I could define the bootstrapping props I needed for a given sub-app in one place that would be automatically included on initial page loads and then dynamically omitted.

The Problem

In Inertia, partial reloads work in conjunction with the following three types of items in the component's props array (as seen here):

php
return Inertia::render('Users/Index', [
    // ALWAYS included on first visit...
    // OPTIONALLY included on partial reloads...
    // ALWAYS evaluated...
    'users' => User::get(),

    // ALWAYS included on first visit...
    // OPTIONALLY included on partial reloads...
    // ONLY evaluated when needed...
    'users' => fn () => User::get(),

    // NEVER included on first visit...
    // OPTIONALLY included on partial reloads...
    // ONLY evaluated when needed...
    'users' => Inertia::lazy(fn () => User::get()),
]);

As you can see, if simply wrap our bootstrapping props in an anonymous closure, that would work - but only for situations in which the bootstrap data is only needed for a single page component. Since closures are always included on first visit, that means they will be included on every sub-app page navigation.

Alternatively, we could wrap the bootstrapping props in an Inertia::lazy() statement; with this, the props will never be included on initial page loads, so that solves the problem that closures have. However, the only way to include a prop that's wrapped in an Inertia::lazy() statement is to do a partial page reload; i.e. we'd have to do the initial page load, check if we need the bootstrapping props, then do a reload if necessary. This is the same process I was using previously, albeit with standalone API calls, and it brings us right back to square one.

The Solution

Seeing as I absolutely hate drudging up the same boilerplate code over and over again, especially when I think there's an alternative approach surely within reach, I decided to write a helper service to handle this logic for me.

Props Structure

First I needed to think about what the props structure would need to look like for this to work. I wanted to load my bootstrap props only on initial page load; this means that if someone pasted a URL to any page in a given sub-app's page components, it would include these props.

In addition to that, I wanted to omit these sames props on all subsequent page loads within the same sub-app's page components.

So to boil the problem down even further: the bootstrap props need to be included in the absence of some sort of special flag that we will pass to Inertia on subsequent page loads, and it can't be a GET URL parameter because if someone saves that as a book mark or something similar, the page would be broken when that URL was visited, since the bootstrapping props would be omitted.

In the end, what I wound up using was a new props structure:

php
use Inertia\Inertia;

class SomeAppController extends Controller
{
    public $initProps;

    function __construct()
    {
        $this->initProps = [
            'init' => Inertia::lazy(fn() => [
                'bootstrapProps1' => SomeService::fetchData(),
                'bootstrapProps2' => 'https://youtu.be/dQw4w9WgXcQ?si=rjb6gH2LzPRvXLT7',
            ]),
        ];
    }

    public function index(Request $request)
    {
        return Inertia::render('SomeApp/SomSubApp/Page',  InertiaDataService::parseProps([
            'nonBootstrapProps1' => SomeOtherService::fetchData(), 
            'nonBootstrapProps2' => 'Hello There',
            ...$this->initProps # <-- Bootstrap props easily included via spread operator
        ]));
    }
}

INFO

The bootstrapping props must be in an array wrapped in Inertia::lazy(), and each item within that array must be keyed with a unique key name that won't clash with any page component's props they would be merged with

With this setup, I'm assuming that if you have a "sub-app" or something like that which shares a single Pinia store (or some other state management system/store) for a common purpose, you are probably including all of these page calls within the same Laravel Controller. If not - well, you could setup a dedicated file to statically hold your bootstrap page props I suppose.

Either way, what we're doing here is simple - we're defining the sub-app's bootstrapping props in a single place for easy reuse while also wrapping them in an Inertia::lazy() so that not only will they have to be manually included, they also will not be evaluated unless needed, so we can rest assured that we will only be evaluating and shipping the necessary bootstrapping data when required.

The only thing necessary to add these bootstrapping props into the Inertia::render() call is to include these props with any other props via the spread operator. If a given page doesn't have any props, you would just include these initProps directly.

Of course, you are free to structure the props array however makes the most sense, just as long as the bootstrapping props adhere to the following:

  • They're in an "init" keyed array item
  • That item is an Inertia::lazy() function with another array inside with the bootstrapping props
  • Each bootstrapping prop has a unique name with will not clash with another prop on the given page component's props.

So this will work:

php
$props = [
    'foo' => SomeService::getStuff(),
    'bar' => SomeOtherService::getSomeOtherStuff(),
    'init' => Inertia::lazy(fn() => [
        'biz' => SomeBootstrapService::bootstrapTheThings()
        'bat' = 'General Kenobi',
    ]),
];

However this will not work because "foo" would be included twice:

php
$props = [
    'foo' => SomeService::getStuff(),
    'bar' => SomeOtherService::getSomeOtherStuff(),
    'init' => Inertia::lazy(fn() => [
        'foo' => SomeBootstrapService::bootstrapTheThings()
        'bat' = "You've failed me for the last time",
    ]),
];

Of course, you probably noticed the page props are first being passed to the InertiaDataService::parseProps() function - we'll touch on that next.

Inertia Data Service

To dynamically include the bootstrapping props, I setup an InertiaDataService class in Apps\Services:

php
<?php

namespace App\Services;

use Inertia\LazyProp;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;

class InertiaDataService
{
    public static function parseProps(array $props)
    {
        if (Arr::has($props, 'init'))
        {
            // We'll just omit the 'init' property if we sent over this header
            if (request()->hasHeader('X-Inertia-Data-Initialized')) return Arr::except($props, 'init');

            // Otherwise, we'll evaluate the 'init' property and merge the child items
            // with the main props array
            else if ($props['init'] instanceof LazyProp)
            {
                $props['init'] = App::call($props['init']);
                $props = array_merge($props, $props['init']);
                $props = Arr::except($props, 'init');
            }
        }

        return $props;
    }
}

As you can see, it's functioning is not very complicated. It's really just an extension of what Jonathan Reinink and the folks supporting Inertia are already doing beihind the scenes:

  • If the passed props array has an 'init' key, we'll continue processing it, otherwise we just return the props
  • If the request has a custom "X-Inertia-Data-Initialized" header, we'll simply return the passed props with the 'init' item omitted
  • Otherwise if the request does not have that custom "X-Inertia-Data-Initialized" header, only then will we evaluate the props within the 'init' item and return an array with these items merged with the originally passed props.

So for example, if we passed the following array to this service:

php
$props = [
    'foo' => SomeService::getStuff(),
    'bar' => SomeOtherService::getSomeOtherStuff(),
    'init' => Inertia::lazy(fn() => [
        'biz' => SomeBootstrapService::bootstrapTheThings()
        'bat' = 'General Kenobi',
    ]),
];

return InertiaDataService::parseProps($props);

It would return the following:

php
[
    'foo' => SomeService::getStuff(),
    'bar' => SomeOtherService::getSomeOtherStuff(),
    'biz' => SomeBootstrapService::bootstrapTheThings()
    'bat' = 'General Kenobi',
]

However, if our custom "X-Inertia-Data-Initialized" header was included in the incoming request, the following would be returned:

php
[
    'foo' => SomeService::getStuff(),
    'bar' => SomeOtherService::getSomeOtherStuff(),
]

Now for the final piece: actually including this custom HTTP header

Passing The Custom HTTP Header In Vue

Thankfully this is very straightforward; all we need to do to include the custom header for Inertia requests is the following:

For standard DOM links using the Inertia JS Link component:

vue
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
    <Link href="https://youtu.be/MtN1YnoL46Q?si=u3t0kLiy1a6MAPz3" :headers="{ 'X-Inertia-Data-Initialized': 'true' }">Click me</Link>
</template>

For manual visits via the Inertia JS Router:

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

router.visit('https://youtu.be/kfVsfOSbJY0?si=PZNcLMgyObqn35wA',
    {
        headers: {
            'X-Inertia-Data-Initialized': 'true'
        },
    }
)
</script>

So all you need to do is configure that header on your links in your sub-app's pages and voilà - you are now dynamically loading your page's bootstrapping props only on the initial page load.

Of course, exactly how you implement this is up to you.