Its been a while since I wrote a blog post, but during these days we are left with a lot of free time while staying in the comfort of our homes so I decided to write some stuff up.
If you are a Laravel developer, you have probably heard of Inertia.js, the modern monolith. With Inertia, you can quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers. This makes Inertia a perfect choice for building some web apps.
Inertia gained a lot of traction in the past few months, so I have decided to give it a shot and build something with it. Installing and configuring it was easy, the documentation is awesome and I had no difficulties setting up my project. It took me a little bit of time to adapt to the mental model of how Inertia works, but after wrapping my head around it I started building the app.
Building my first page and the need for a layout
Every app you have built has a layout that you wrap around your pages. Usually, the Layout has the Header
the Footer
and a slot
(or router-view
) tag where you insert the content based on the route you are in. In the documentation, there is a straightforward example of a page, which you can also see attached in the code below.
<template>
<layout title="Welcome">
<h1>Welcome</h1>
<p>Hello {{ user.name }}, welcome to your first Inertia app!</p>
</layout>
</template>
<script>
import Layout from './Layout'
export default {
components: {
Layout,
},
props: {
user: Object,
},
}
</script>
As you can see from the example, you need to wrap every page with the Layout, insert that layout in the script
tag and add it to the components
key. This gives you the flexibility that you might need if you are using different layouts, but what if we could find a better, more convenient way to make this happen?
The first (and flawed) approach I went for
To use Inertia in your Vue app, you just have to copy and paste their code snippet from the documentation and you are good to go.
import { InertiaApp } from '@inertiajs/inertia-vue'
import Vue from 'vue'
Vue.use(InertiaApp)
const app = document.getElementById('app')
new Vue({
render: h => h(InertiaApp, {
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: name => require(`./Pages/${name}`).default,
},
}),
}).$mount(app)
Lets dive deep into this code so we can try to understand what is happening, so you can understand why I thought of this solution first.
First, we include the InertiaApp
and inject it in our Vue
instance. Then, we get the selector
we want to $mount
our Vue app in, and instantiate our Vue
instance. The object we pass to the constructor has only one property, and its the render
property, which as it states, renders our the app. It has one argument, h
which is the render function
we call to render our InertiaApp
component.
If you never used the render
function in your Vue apps, it can take three arguments, first one is a Vue instance
(Vue Component), the second one is the data
passed to that component, and the third one is an array
of children included in the component.
As you can see in the code above, we are rendering the InertiaApp
render: h => h(InertiaApp, ...)
we pass some data
as the second argument, and there are no children rendered for this component.
My initial approach was, what if we wrap the whole InertiaApp
in the Layout
we are going to use. The end code snippet looked like this
import App from './layout/App';
new Vue({
render: (h) =>
h(App, {}, [
InertiaApp,
{
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: (name) => require(`./Pages/${name}`).default
}
}
])
}).$mount(app)
Now, we don’t have to include our Layout in every component. But, this approach is flawed and I am going to explain why.
The first flaw of this approach is because this approach is not flexible. Meaning that you will be stuck with the same layout on every Inertia page you build. This flaw can limit you in many ways.
The second, and deeper flaw that I have found with this approach is that you cannot access the $page
property within your layout. The $page
property, contains shared data you pass from your backend to your frontend. For example, if you want to show a simple Welcome {username} message in your Header (which is in the Layout), you need to share the authenticated user from the backend to the frontend. But, if you try to access your $page.user
property within your Layout you will get an error saying that can not get property user
of undefined. At first, this error didn’t make any sense. I can use the $page.user
property in my pages
, but I cannot use it in my Layout. After debugging for some time and looking at the Inertia’s source code, I have found out that internally Inertia injects the $page
property Object.defineProperty(Vue.prototype, '$page', { get: () => app.props })
in our Vue instance, which is a getter
that returns the props
of the app
, and if you go back to the previous code snippet, we pass the shared data from our backend in our InertiaApp
component as a prop
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: (name) => require(`./Pages/${name}`).default
}
which are then parsed and injected in the Vue instance. To simplify the flaw, the Layout did not had access to the shared data passed from our server.
Solving the flaws with a convenient solution
Lets get back to our snippet above. We resolve the current page with this code
resolveComponent: (name) => require(`./Pages/${name}`).default
As you can see, we require the page (as a module), and return the default export from that module. Within that default export we can find all of the current page’s data, including the layout. So, what if we modify the module, include the default layout and then return the modified module? Now, we don’t have to wrap all of our pages with the layout. The code now looks like this
import App from './layout/App'
//
resolveComponent: (name) => {
const module = require(`./Pages/${name}`);
module.default.layout = App;
return module.default;
}
Now, we have set a default Layout for all of our Inertia pages. But, we still have not solved any of the flaws we mentioned previously. Lets do that.
To solve the first flaw, we can modify the code snipped above in a way that allows us to add the default Layout only if there is no layout already defined.
resolveComponent: (name) => {
const module = require(`./Pages/${name}`);
if(!module.default.layout) {
// there is no Layout defined, set the default layout
module.default.layout = App;
}
return module.default;
}
We now achieved flexibility and solved the first flaw.
As for the second flaw, if we have a look at the source code for the inertia-vue/src/app.js
you can see that we have the following peace of code in the render
function
if (this.component.layout) {
if (typeof this.component.layout === 'function') {
return this.component.layout(h, child)
}
return h(this.component.layout, [child])
}
This code ensures that the all of the props are transformed and injected in our app before we render the layout, so now we can access our $page
property in the Layout as well.
At the end, we have ended up with the following code snippet.
import { InertiaApp } from '@inertiajs/inertia-vue'
import Vue from 'vue'
import App from './Layouts/App';
Vue.use(InertiaApp)
const app = document.getElementById('app')
new Vue({
render: h => h(InertiaApp, {
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: (name) => {
const module = require(`./Pages/${name}`);
if(!module.default.layout) {
module.default.layout = App
}
return module.default;
}
},
}),
}).$mount(app)
I didn’t want to give you the solution right from the start because there was a lesson to be learned from the first approach I took, so maybe in the future you can have a gotcha moment if you ever stumble across an issue like this.
And remember, stay safe, stay healthy and stay home. We can only defeat this pandemic together.