Jonathan
Reinink

Introducing Inertia.js

Posted on March 19, 2019

I recently wrote an article explaining how to do full client-side rendering in classic server-side applications. My goal with this approach is to allow developers to build rich client-side apps without all the complexity of building a full-on single-page app with accompanying API.

I wanted to blend the best parts of classic server-side apps (routing, controllers, and ORM database access) with the best parts of single-page apps (JavaScript rendering and no full page reloads). Or, at least the best parts as I see them. 😊

That journey has led me to a pretty interesting new pattern, which this article explains in detail. In short, I've created a Turbolinks inspired library that makes it super easy to create server-driven single-page apps. I am calling this library Inertia.js.

If you're more of a visual learner, here's a video walk-through where I look at a demo app I've created, called Ping CRM, to help illustrate this pattern.

Framework agnostic

Before I go any further, I should mention that Inertia is both server-side and client-side framework agnostic. It's not intended to replace your existing frameworks, but rather to complement them. And while the rest of this article will use Laravel and Vue.js as examples, you'll be able to use Inertia with any server-side framework (e.g. Rails, Django, Laravel, Symfony), as well as any client-side framework that supports dynamic components (e.g. React, Vue.js). More on that in a bit.

Client-side rendering recap

To set the stage, let's start with a quick recap.

I've recently abandoned server-side rendering entirely in my Laravel projects in favour of a fully Vue.js based front-end. There's a base template, which contains a single div, used as the root Vue.js container. That div has two data attributes, a component name, and component data (props). This is used to tell Vue.js which page component to display, and also provide the data (props) required for it.

Here's what that base template looks like:

<html>
<head>
    // …
</head>
<body>

<div id="app" data-component="{{ $component }}" data-props="{{ json_encode($props) }}"></div>

</body>
</html>

Next, in my controllers, instead of returning a server-side rendered view, I now return the base template with a component name and data (the props). Here's an example of a controller:

class EventsController extends Controller
{
    public function show(Event $event)
    {
        return View::make('app', [
            'component' => 'Event',
            'props' => [
                'event' => $event->only('id', 'title', 'start_date', 'description'),
            ],
        ]);
    }
}

The beauty of this approach is that it allows you to still use classic server-side routing and controllers, as well as your typical database ORM for data access (without the need for an API), all while enjoying many of the benefits of a fully JavaScript rendered view layer. Be sure to read the full article for a better understanding of how it all works.

Taking it to the next level

As I was working with this new approach, there were three areas that I wanted to improve on:

  1. I wanted better single-page app support. Using Turbolinks was neat, but it started feeling more and more like a hack. I was running into unexpected negative side effects. For example, the Vue.js devtools were not working between page visits. And really, this makes sense. Turbolinks was never intended for this use-case.
  2. I was seeing the odd page flicker between page visits. Not a big deal on one hand, but on the other hand I wanted this approach to feel silky smooth. This flicker was simply caused by the time required to load and execute JavaScript between each visit.
  3. I was concerned at how large the JavaScript bundle size could get in large projects. I tried using code splitting, but that resulted in a significant flicker between page loads. Again, this made sense, since on each page visit Vue.js had to boot, load the page component, determine which child components were required, and then asynchronously load them.

As it turns out, all these issues were related to a single core problem: the fact that I was clobbering my Vue.js instance on each page visit and having to reboot it.

This got me thinking. What if I could maintain a persistent Vue.js instance by loading all subsequent page visits over XHR, returned as JSON (not as HTML like Turbolinks), and then simply hot-swap the page component and data (props)? Doing this would avoid all flickering, since each page transition would be handled entirely lazily.

Enter Inertia.js

It was at this moment I knew I was onto something neat and decided to create a new, Turbolinks inspired, library. Enter Inertia.js Here's how it works:

  1. On the first page load, the base template is rendered by the server, and the current page component is then loaded from the root div, which includes the page component name and its data (props).
  2. When a user clicks a link, I intercept that click event, prevent the default behaviour (a full page visit), and instead make an XHR GET request to the link's URL.
  3. The server detects that this XHR request is actually an "Inertia request", and instead of returning the full base template, it instead only returns the new page component name and props as JSON.
  4. On the client, Inertia dynamically swaps the existing page component and props with the new page component and props that were returned from the XHR request.
  5. Once the new page has loaded, Inertia updates the browser history using push (or replace) state.

Pretty cool, right? Let's unpack it a little further.

Hot-swapping page components

As it turns out, both Vue.js and React already have first-class support for dynamic components. Here's what dynamic components in Vue.js look like:

<component v-bind:is="page.component"></component>

This can also be written as a render function:

render(h) {
  return h(this.page.component, { props: this.page.props })
}

Under the hood this is how Inertia is hot-swapping the page components.

Code splitting

While not a requirement, I'm happy to report that code splitting works perfectly with Inertia. If you're not familiar with code splitting, here's a crash course:

On the first page load, the smallest JavaScript bundle possible is sent. In my app this would include my app.js file, Vue.js, and any other critical dependencies needed to boot the app.

From there, whenever a new component is needed, it's lazy-loaded onto the page. Meaning you only ever download the JavaScript actually required for the page you are viewing. Nothing more.

This is all made possible since each component of your app is split into its own JavaScript file at the point of bundling.

Inertia provides a resolveComponent callback where you define how each page component module is loaded. You can enable code splitting in this callback by using dynamic imports:

resolveComponent: (component) => {
  return import(`@/Pages/${component}`).then(module => module.default)
}

Loading indicator

Since all requests are now being made via XHR, there's no default browser loading indicator. To solve this, Inertia ships with NProgress.js, a nice little progress bar library. It's only displayed if a request takes longer than 500ms. Here's an example with a page intentionally delayed 2 seconds:

If you're familiar with Vue Router, React Router, or even Turbolinks, they all intercept the default browser link click behaviour in order to preserve the current page, and then instead make XHR requests to load the requested page. Inertia works the same way.

To create an Inertia link, you do this:

<inertia-link href="/">Home</inertia-link>

Which, conceptually under the hood, is simply an anchor tag with a click event:

<a href="/" @click.prevent="visit">Home</a>

You can even specify the browser history and scroll behaviour. By default all link clicks "push" a new history state, and reset the scroll position back to the top of the page. However, you can override these defaults using the replace and preserve-scroll attributes:

<inertia-link replace preserve-scroll href="/">Home</inertia-link>

Manually making Inertia visits

In addition to clicking links, it's also very common to manually make Inertia visits. For example, after a successful login form submission, you may want to "redirect" to a different page. This is also easily done using the Inertia.visit() helper:

submit() {
  axios.post('/login', this.form).then(response => {
      Inertia.visit(response.data.intendedUrl)
  })
},

And just like with an <inertia-link>, you can also set the browser history and scroll behaviour:

Inertia.visit(url, {
  replace: true,
  preserveScroll: true,
})

In fact, since "replace" is a more common action, you can even do this:

Inertia.replace(url, {
  preserveScroll: true/false,
})

Responding to Inertia requests

Now that we know how to make Inertia requests, let's look at how we get our server-side framework to respond properly. As it turns out, much of the groundwork has already been done, since each page already only returns a single page component and props.

Here's what we've been doing to this point. Each time we display a page in our app, we simply display our base template (app.blade.php) and include the component name and data (props).

return View::make('app', [
    'component' => $component,
    'props' => $props,
]);

However, for an Inertia (XHR) request, we don't want to return the entire base template. Instead, we only want to return the component name and props as JSON. This is possible by checking for the X-Inertia header:

if (Request::header('X-Inertia')) {
    return Response::json([
        'component' => $component,
        'props' => $props,
        'url' => Request::getRequestUri(),
    ], 200, [
        'Vary' => 'Accept',
        'X-Inertia' => true,
    ]);
}

return View::make('app', [
    'component' => $component,
    'props' => $props,
]);

Note how we also include the X-Inertia header on the response. This tells Inertia on the client that it's a proper Inertia response, and not, for example, an error page. We also include the current page url in the response, since redirects may have happened on the server, and we want to update our browser history with the correct final URL. Yes, you read that correctly, this approach even works if there are server-side redirects!

I'm showing this full example to illustrate how easy it is to implement Inertia in any server-side framework. However, my goal is to ship helper libraries for popular server-side frameworks. For example, in Laravel all you'll need to do is call Inertia::render() in your controller:

class EventsController extends Controller
{
    public function show(Event $event)
    {
        return Inertia::render('Event', [
            'event' => $event->only('id', 'title', 'start_date', 'description'),
        ]);
    }
}

Sharing global data

A common question I received from my previous article was how to handle global view data. For example, maybe you want to display the currently authenticated user in the site header.

The recommended approach here is to include this information on each Inertia request as additional data (props). Just be aware that Inertia doesn't scope this shared data, so you'll need to do that yourself. I've done this using app and auth keys.

To avoid manually returning the shared data in each controller response, I've created a helper to define the shared data ahead of time (e.g. in your app service provider):

// Synchronously
Inertia::share('app.name', Config::get('app.name'));

// Lazily
Inertia::share('auth.user', function () {
    if (Auth::user()) {
        return [
            'id' => Auth::user()->id,
            'first_name' => Auth::user()->first_name,
            'last_name' => Auth::user()->last_name,
        ];
    }
});

How you access this shared data is going to be different depending on your client-side framework. In Vue.js, I handle this using the provide and inject pattern. The page itself is "provided", which you can then "inject" into any component that needs access to the shared (global) data (props). Practically speaking, I've only needed this in my main <layout> component:

<template>
  <div>
    <header>
      Welcome, {{ page.props.auth.user.first_name }} {{ page.props.auth.user.last_name }}
    </header>
    <nav>
      <!-- ... -->
    </nav>
    <article>
      <!-- ... -->
    </article>
  </div>
</template>

<script>
export default {
  inject: ['page'],
  // ...
}
</script>

Handling non-GET requests

Out of the box, Inertia doesn't do anything special for POST, PUT, PATCH or DELETE requests. That is to say if you perform a traditional form submission and return a redirect from the server, you're going to see that redirect happen on the front end along with a full page reload, just like you would in a regular server-side app.

If you want to take full advantage of Inertia's persistent-process model, I recommend submitting forms using XHR with a library like Axios instead of doing classic form submissions.

This comes with other benefits as well such as being able to modify and transform form values client-side before sending them to the server. Error handling is also really easy. When the server returns a 422 response with errors (as JSON), simply update the form errors data attribute to reactively display them. No need to repopulate the form with past values!

Handling exceptions

Another interesting piece with this whole approach is exception handling. One of the things I love about working with Laravel is the built-in exception handling you get for free. Laravel ships with Whoops, a beautiful error reporting tool which displays a nicely formatted stack trace in local development.

The challenge is, if you're making an XHR request (which Inertia does), and you hit a server-side error, how do you display that? The typical way you handle this in a single-page app is to return the error exception as JSON, and then have a client-side exception handler that displays it somehow.

Inertia takes a bit of a different approach. By default, whenever Inertia receives a non-Inertia response (meaning there's no X-Inertia header present), it simply shows that response in a modal. Meaning you get the same error-reporting you know and love, even though you've made that request over XHR! Here's a quick demo:

This behaviour is primarily intended for development purposes. In production, you'll want to return a proper Inertia error response. To do this you'll need to override your framework's default exception handler response with something like this:

public function errorHandler($exception)
{
    return Response::json([
        'component' => 'Error',
        'props' => [
            'code' => $exception->getStatusCode(),
            'message' => $exception->getMessage(),
        ],
        'url' => Request::getRequestUri(),
    ], 200, [
        'Vary' => 'Accept',
        'X-Inertia' => true,
    ]);
}

Long term, I see this being another helper in the framework specific adapters.

Generating URLs from routes

Since all routing is handled server-side, generating links can be a little tricky in your client-side components. That said, here's something I do all the time:

<inertia-link :href="route('users.create')">Create User</inertia-link>

This is made possible in Laravel using a library called Ziggy. This library makes all your server-side route definitions available in JavaScript. You can also use this library to set active link states:

<inertia-link :class="{ active: route().current('users') }" :href="route('users')">Users</inertia-link>

Closing thoughts

So there you have it, I hope that gives you a good idea of what Inertia.js is all about! I really feel like this approach strikes a nice balance between classic server-side apps and modern single-page apps. It allows developers to build rich single-page client-side apps, without having to build a full REST or GraphQL API, or learn client-side state management, routing, and really much of what comes with modern SPAs.

As for when Inertia.js will be ready for public consumption, I can't give a definitive answer yet. It's still quite early in the development process. If you want to be notified about future blog posts, which would include any Inertia.js announcements, be sure to join my mailing list below.

If you want to be notified in the future about new articles, as well as other interesting things I'm working on, join my mailing list!
I send emails quite infrequently, and will never share your email address with anyone else.