Advanced Inertia
Advanced Inertia
Table of contents
Foreword 6
Introduction 7
Dealing with types 10
Why TypeScript? 11
Types and stuff 15
Laravel and Vue.js 28
VS Code Support 34
The Workflow 35
VS Code 35
Vite 37
Prettier 38
ESLint 46
Structuring apps 59
Switching the structure 61
Naming guidelines 72
Composition API 76
Separation of concerns 80
Reactivity 83
Computed properties 85
Watching changes 87
Props 88
Emits 89
TypeScript support 90
Mixins 93
The HOT take 94
Concepts 95
Singletons 95
Composables 98
Utils 100
Plugins 102
Working with data 108
Laravel Data 111
TypeScript Transformer 112
Resources 114
Requests 117
Automating the generation 124
Wrapping it up 126
Pagination 127
On the frontend 128
Authorizations 131
Policies 132
Data Resources 133
TypeScript definitions 136
Down the frontend 137
Routing 138
Momentum Trail 139
Auto-generation 143
Global state 144
Flash notifications 149
Handling on the frontend 156
Cleaning things up 157
Auto imports 158
Global components 162
Conclusion 165
Afterwords 167
6 Advanced Inertia
Foreword
Before we start, even though I consider this guide quite good even for
beginner-level developers, it assumes that you know the Inertia.js basics and
have had experience with JavaScript frameworks before.
Examples from the book are written using Vue 3 syntax, but described
concepts could be applied to any modern framework, whether you use React,
Svelte, or even Vue 2. More than that, the guide will still work great if you’re not
into Inertia and only building API-powered apps.
Note for Vue 2 developers
Vue 2 is still actively utilized within the Laravel community. However, I would
advocate switching to Vue 3 soon since the previous version is reaching end-
of-life in 2023 and can’t compete with the current one in terms of developer
experience.
Advanced Inertia 7
Introduction
Hi,
I’ve been developing web applications professionally over the past six years —
some within a team, but the majority as a solo full-stack developer. Some of
these apps had the worst codebases you could ever imagine.
I’ve been through the whole cycle of not-really-a-frontend-developer, starting
from embedded <script> in HTML, jQuery, and some hideous custom
solutions to mastering Vue and Inertia.js to the level where I can come back to a
project a year later and not get anxious about the code quality right away. Vue
3 is my preference, but the book’s examples apply to most existing frameworks.
8 Advanced Inertia
When writing pure Laravel code, you have many built-in out-of-box features,
such as routes, localization, permissions, and stuff. With proper IDE, third-party
extensions, and tools, you are granted the best developer experience on the
market, including type checking, static analysis, syntax highlighting, and auto-
completion.
But the most important thing — you have consistency. If you do something
“right,” you always know what data you have to deal with, what shape it has,
and what results to expect.
You’re missing a huge opportunity with an “external” JavaScript-based
frontend, whether it’s an Inertia-powered or API-based app. More than that,
even though Laravel has the first-class support of several frontend frameworks,
and such solutions as Vue and React are widely adopted within the community,
there are still no general conventions on how to do things the “right way.”
With this book, I’m going to guide you through building a connecting layer
between Laravel and Inertia-powered frontend to make monolith applications a
breeze to develop and maintain and bring back the feeling of working on a
single codebase.
10 Advanced Inertia
Why TypeScript?
TypeScript provides optional static typing, which lets you better structure and
validate your code at the compilation stage. It also brings IDE autocompletion
and validation support along with the code navigation feature. In short,
TypeScript can highlight unexpected behavior in your code and improve the
debugging process during the development stage.
What’s not less important, TypeScript helps you better understand the code
you wrote previously so that you won’t have to guess what data you were
working with or expecting.
TypeScript used to scare the hell out of me, and honestly, it still does.
Below is a perfect example of what average TS code looks like for beginners,
and it reflects how I used to see any typed code. So, it was easier to continue
writing pure non-typed JS (even though I’ve already gotten used to typing
things on the backend) instead of spending hours learning that monstrosity
without knowing exactly what value it may bring.
12 Advanced Inertia
Does this snippet below somehow encourage you to start learning TypeScript? I
doubt.
Why would you ever need to write such a craziness? You would not.
Advanced Inertia 13
The truth is that I still don’t know TypeScript. I mean, I kind of understand basic
types and can write simple interfaces, but I’m not an expert in complex type
gymnastics. The best part is that you don’t need to know much about
TypeScript to benefit from it and start creating a connecting layer between your
backend and the frontend.
In fact, you only need to know TypeScript to the same extent you already know
the PHP type system.
So, what value can you get from writing typed code? As a quick example,
consider we have a method defined outside the current component.
function showNotification(payload)
{
// do things
}
A common situation is when you reuse the code you wrote some time ago, you
can’t reliably say what data it accepts and what results you will get from
executing it without jumping to the original definition.
Is payload even a string or an object?
What properties should it have?
What results to expect from the execution?
The answers are usually obvious when writing the code, but you only have to
hope you’ve got all the details right over time. Otherwise, you have to jump over
definitions again and again.
14 Advanced Inertia
If you make a mistake, IDE and the compiler will notify you about the wrong
type provided, decreasing the chances of getting bugs.
The example above is one of many possible cases when you have to guess or
explore code instead of just writing new stuff. With typed code, you help future
yourself better understand the code you write.
Advanced Inertia 15
PHP
If you’re into modern PHP, you should already be familiar with its typing system.
With PHP 7.4 and above, we can natively type hint:
Function arguments
Return values
Class properties
Say you’re building an online store and has extracted a function into a
dedicated custom action:
class CreateProduct
{
public static function execute(
Category $category,
ProductData $data
): Product
{
return $category->products()->create($data);
}
}
16 Advanced Inertia
Once you type CreateProduct::execute( , the IDE will inform you about
what type the next argument should have and notify you if the payload doesn’t
comply with the expected type.
Even if you haven’t spotted the bug during the development, you will still get a
runtime exception telling you what’s wrong with the code as soon as you call
the method.
The method above also has information about its return type, ensuring that
once it’s executed, the result will have the desired type.
Due to historical reasons and the text-based nature of HTTP requests, PHP has
a weak typing system. Meaning, that when it comes to simple scalar types, PHP
is ok with every argument being any scalar type by default, and will convert it
into a required format later. PHP supports such primitives as int , float ,
string , and bool .
$value = 'on';
This behavior may lead to some non-obvious bugs, but is luckily preventable by
enabling strict types:
However, declaring strict types in PHP only enforces you to pass correct types
into a function, but it doesn’t prevent you from making mistakes on the variable
level.
That’s still a valid code and will be executed with no exceptions thrown:
declare(strict_types=1);
TypeScript
Unlike PHP, TypeScript is strongly typed, which means that the compiler
doesn’t perform any type transformations and will expect every variable or
argument to be processed strictly following their types.
In TypeScript, you can natively type variables and arguments and return values.
Once the variable’s type is declared, you cannot assign it to any other value
except what the defined type allows you to.
Since TypeScript is just JavaScript on steroids, it already knows what’s
happening in your code. Even if you don’t type a variable explicitly, once you
assign it to a particular value, TypeScript will take a note and would treat it as
the same type that value has. If you set a variable value to true , the compiler
would expect it to be a boolean in further use and will prevent possible bugs.
20 Advanced Inertia
So, it’s up to you if you want to type variables with simple scalar types at all. In
many cases, TypeScript can already infer types from assigned values.
TypeScript supports such common concepts as:
Types
Enums
Interfaces
Generics
Don’t be afraid; we are only going to review a few basic concepts to give you a
general idea of how to implement these things in practice. My favorite part of
this guide is that you will not need to write any custom types.
If you want to dig deeper into TypeScript theory, there’s an official handbook,
which explains all the basics in a short way.
Basic types
TypeScript inherited three main primitives from JavaScript, and you should
already be familiar with:
number
string
boolean
Union types
Sometimes, you can run into a situation when a function argument has to
accept multiple types. The most obvious example is a parameter that could be
either string or number .
We can define union types using the | pipe operator.
In TypeScript, you can also declare new types. To implement the type alias, use
the type keyword to create a new type.
Object types
In JavaScript, we often group and pass data around via objects. That’s a
common situation when you don’t exactly know what properties should the
passable object have.
function publish(payload) {
const title = payload.title;
}
TypeScript lets declare type aliases and interfaces to represent the shape of
data. Coming from PHP, it’s easier to think of object types as data objects.
type User = {
id: number
email: string
image: string | null // nullable property
team_id?: number // optional property
}
In rare cases, when the object is only used once, you can even omit the
declaration and define the type anonymously:
TypeScript object types are slightly different from PHP. When you type an
argument in PHP, it assumes that you pass a class instance that implements
that interface or extends the specified class. With TypeScript, the method will
just expect an object with the same properties and methods, even if it has extra
attributes.
type Tweet = {
body: string
}
interface Tweet
{
body: string
}
Arrays
In JavaScript, you can never be sure what data an array contains. TypeScript
lets you type the shape of arrays as well.
type Tweet = {
body: string
}
publishAll([
{ body: "Hello world!" },
{ body: "That's my second tweet" },
])
You also can use the Array<T> generic instead. Both approaches are
identical, but one could be more convenient to use than the other in different
cases.
Any
TypeScript has a special type called any that can be used when you don’t
explicitly know what type should be applied and don’t want a particular value to
cause type-checking errors.
// A valid code
title = true;
Generic types
Generics are the only thing I miss in PHP now. Generics help you create
reusable components that need to accept different types. In short, generics are
compound types that depend on other types.
Generics can be used in functions, types, classes, and interfaces. That’s quite
an extensive topic, and we will only review the basic usage of generic types
within this guide.
Say you expect paginated data from a backend. We know that Laravel wraps
provided data into a special object that contains some additional metadata:
first_page_url
prev_page_url
next_page_url
last_page_url
and other properties
Advanced Inertia 27
Assuming you have several pages that utilize pagination, you would have to
define separate types for each use case. With generics, you can create a single
type that reuses data from other type(s) provided.
type Paginator<T> = {
data: T[]
meta: {
first_page_url: string
last_page_url: string
next_page_url: string | undefined
prev_page_url: string | undefined
}
}
In the example above, T represents a passed-in type. So, TypeScript will treat
the data property depending on the context.
type Tweet = {
body: string
}
Dealing with typed data with real-world use cases is described within the
Working with data chapter.
28 Advanced Inertia
With pure Vue.js or React installation, you have an interactive installer that
helps you automatically plug in the TypeScript support. With a frontend directly
integrated into Laravel, you need to install it manually.
Adding TypeScript support to your Laravel project is simple and only takes
minutes. Let’s review the process with a fresh Laravel Breeze install with Vue 3.
There’s no issue with starting from a clean Inertia.js installation, but I would
recommend trying it out with Laravel Breeze first to ensure everything
works perfectly immediately.
npm i -D typescript
Advanced Inertia 29
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"types": ["vite/client", "node"],
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
}
},
"include": [
"resources/**/*.ts",
"resources/**/*.d.ts",
"resources/**/*.vue"
]
}
30 Advanced Inertia
Switch to TypeScript
Initial Laravel installation comes with a boilerplate JavaScript entry point, which
needs to get converted into TypeScript. All you need to do is rename .js to
.ts and make slight changes to the Vite config.
resources/js/app.js
resources/js/app.ts
Advanced Inertia 31
You also may want to instruct the compiler and the IDE to interpret your
components' code as TypeScript. Append the lang="ts" part to the
component’s script part.
<template>
...
</template>
From this point, Vite can parse and compile TypeScript code in your Vue
components without any extra steps.
Linting code
Even if your IDE can help you with real-time syntax highlighting and
autocompletion, there’s still room for mistakes and type errors. We use a CLI
tool vue-tsc to check them during the building process.
vue-tsc is a Vue-specific wrapper around tsc — a utility for command line
type checking and type declaration generation.
npm i -D vue-tsc
Advanced Inertia 33
To run the check manually, type the following command in the command line:
vue-tsc --noEmit
The --noEmit flag instructs tsc to not build output files and only perform
the checks.
To perform the check automatically every time you build a project, modify the
build script in the package.json file:
{
"scripts": {
"build": "vite build",
"build": "npm run type-check && vite build",
"type-check": "vue-tsc --noEmit"
}
}
That is it; you are all set! You can keep writing code the way you used to and
start utilizing TypeScript features step-by-step.
34 Advanced Inertia
VS Code Support
The Workflow
VS Code
This section is only applicable to VS Code users. You may skip it if you use
a different IDE or code editor and have no plans to switch.
Even though PHP Storm is the number one tool for writing PHP and building
Laravel projects today, VS Code is hands down the best existing IDE for
building frontend. Taking that Inertia.js code, including frontend components, is
as much an essential part of your application as a backend is, VS Code stays
the perfect choice for building apps for many Laravel developers and me
personally.
36 Advanced Inertia
In this book, we are going to review several common tools and IDE extensions
that help us glue two codebases together and make the development process a
breeze, including:
Volar (Vue Language Features)
TypeScript
Prettier
ESLint
Please note that some of the described steps are only applicable to VS Code.
If you are switching from Vue 2, there’s a chance you are still using the
extension called Vetur. Please note that Vetur doesn’t support Vue 3 and is not
being maintained anymore. Instead, it’s recommended to use the new official
VS Code solution — Volar (Vue Language Features).
The installation process is pretty straightforward — just install the extension,
and you’re all set. Volar can understand both Options API and Composition API,
along with the short <script setup> syntax.
Advanced Inertia 37
Vite
Vite is an essential part of the toolkit described in this guide, and I highly
recommend switching to it soon. Thanks to the Laravel team, Vite has already
replaced Laravel Mix in the recent framework versions.
Please refer to the official documentation and migration guide to switch from
Laravel Mix (Webpack) to Vite.
38 Advanced Inertia
Prettier
A decent amount of developers came to Vue.js from Laravel. For some reason,
the official starter boilerplates are still missing all the common tools clean Vue
or React installations have, and the great part of developers keep the default
setup as is.
Whether you are a solo developer or work within a team, it’s great to have not
only consistency in your code structure but also in its formatting style. A
consistent formatting style makes code more readable and predictable, making
it easier to locate logical blocks and HTML tags.
Developers in a team might work in different IDEs and have different
preferences, which may lead to unnecessary changes in commits, such as
different indentations or line breaks. Strict formatting rules ensure that your
code has a consistent style even when developed by other developers.
Advanced Inertia 39
Prettier can automatically rewrite your code and format it in accordance with
defined formatting rules, including but not limited to the following:
Indentation
Line width limit
Semicolons at the ends of statements
Quote types
Trailing commas
Prettier keeps you from hesitating about making formatting decisions yourself,
letting you freely write the code the way it’s quicker and easier for you, and
automatically fixing the style when you’re done.
Installation
The installation part is quite simple and only takes minutes to set everything up.
Install Prettier dependency
npm i -D prettier
# or
yarn add -D prettier
{
"semi": false,
"bracketSameLine": true,
"printWidth": 120,
"singleAttributePerLine": true
}
{
"build": "vite build",
"format": "prettier \"resources/**/*.{js,ts,vue}\" --write"
}
Now, you can run the command npm run format in your terminal to perform
formatting.
To run the formatter automatically every time you build a project, you can
modify the build script as well:
{
"build": "npm run type-check && npm run format && vite build",
"format": "prettier \"resources/**/*.{js,ts,vue}\" --write"
}
At this point, the build script has grown and is going to grow even more. To
shorten the command, we can use the tool npm-run-all .
First, install the package as a dev dependency:
npm i -D npm-run-all
# or
yarn add -D npm-run-all
42 Advanced Inertia
Now, rewrite your build script to make it shorter and easier to read and
understand.
{
"scripts": {
"build": "npm run type-check && npm run format && vite build",
"build": "run-p format type-check && vite build",
}
}
Tailwind CSS
When prototyping things with Tailwind, you may find yourself adding classes
sequentially one by one and not caring much about their placement within the
class property. However, when it comes to working on existing templates and
refining things, you expect to have specific classes in specific places.
Tailwind CSS has an official Prettier plugin, which helps automatically sort
Tailwind classes in the logical order, which makes it predictable to navigate
between classes. It takes into account the priority of classes applied, starting
from element’s positioning and size and ending with colors and extra stuff.
So, basically, Prettier can turn an unpredictable mess into a beautifully
formatted class list:
npm i -D prettier-plugin-tailwindcss
## or
yarn add -D prettier-plugin-tailwindcss
44 Advanced Inertia
IDE extensions
Prettier has the first-class support of all major IDEs and code editors, including:
VS Code
WebStorm (built-in by support)
Atom
Vim
Emacs
Sublime Text
Atom
Like the CLI tool, the extension automatically discovers your configuration and
installed plugins. So, once you install the extension and set it up as a default
formatter, Prettier will start automatically applying rules to your code when you
save a document.
Advanced Inertia 45
VS Code
To ensure that Prettier is used over other formatters you may have installed, set
it as the default formatter in your VS Code settings.
I recommend setting Prettier as a default formatter for every frontend-related
file type. Edit the settings.json file and set the following options:
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
46 Advanced Inertia
ESLint
Over the last few years, such static analysis tools as PHPStan have been widely
adopted within the PHP and Laravel communities specifically. PHPStan scans
the codebase and finds both obvious and tricky bugs, decreasing the chances
of getting runtime errors even in spots poorly covered by tests.
ESLint is a static analysis tool for JavaScript, which takes care of your code
quality, can catch common issues early, and automatically fix some of them. In
short, ESLint helps you spot any bugs like a missing comma or warn you about
a defined variable that is never used.
ESLint doesn’t solve any business logic-related issues but will ensure your code
is free of syntactic errors and follows the best practices. By enforcing code
standard rules across the project, ESLint makes it easier to prevent hard-to-
debug runtime errors for anyone in your team.
Advanced Inertia 47
Unlike Prettier, ESLint is highly customizable, and it doesn’t come with pre-
configured rules. Instead, it’s entirely up to you what potential issues you
consider “issues” at all and what rules to apply to your code, including the
following:
Disallow unused variables
Disallow the use of undeclared variables
Disallow unreachable code after return , throw , continue , and break
statements
Avoid infinite loops in the for loop conditions
Disallow invalid regular expression strings
Install ESLint
To install ESLint in your project, run the following command from the root
directory of your project:
npm i -D eslint
# or
yarn add eslint --dev
48 Advanced Inertia
Configure ESLint
You may either create a configuration file .eslintrc.js manually or initialize
it automatically.
Run the command below to launch an interactive setup. Once you have
answered the questions, ESLint will create a configuration file with pre-
populated options depending on your preferences.
module.exports = {
extends: ["eslint:recommended"],
rules: {}
}
npm i -D eslint-config-prettier
# or
yarn add -D eslint-config-prettier
Then, update your .eslintrc.js file to add "prettier" to the "extends" array.
Make sure to put it last so it gets the chance to override other configs.
module.exports = {
extends: ["eslint:recommended", "prettier"],
rules: {}
}
npm i -D @typescript-eslint/parser
npm i -D @typescript-eslint/eslint-plugin
# or
yarn add -D @typescript-eslint/parser
yarn add -D @typescript-eslint/eslint-plugin
50 Advanced Inertia
module.exports = {
extends: ["eslint:recommended", "prettier"],
rules: {},
parserOptions: {
parser: "@typescript-eslint/parser",
}
}
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier"
],
rules: {},
parserOptions: {
parser: "@typescript-eslint/parser",
},
}
npm i -D eslint-plugin-vue
# or
yarn add -D eslint-plugin-vue
52 Advanced Inertia
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier"
],
rules: {},
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
}
You can use one of the following pre-defined configs by adding them to the
“extends” section:
"plugin:vue/base" — Settings and rules to enable correct ESLint parsing.
"plugin:vue/vue3-essential" — Above, plus rules to prevent errors or
unintended behavior.
"plugin:vue/vue3-strongly-recommended" — Above, plus rules to
considerably improve code readability and/or dev experience.
"plugin:vue/vue3-recommended" — Above, plus rules to enforce subjective
community defaults to ensure consistency.
Advanced Inertia 53
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:vue/vue3-recommended"
"prettier"
],
rules: {},
parserOptions: {
parser: "@typescript-eslint/parser",
},
}
Like with TypeScript, I recommend starting with the “essential” config and
raising the level of strictness gradually when you are comfortable enough with
new rules enforced, so that you don’t get overwhelmed with thousands of
errors on the first run.
Aside from default configs, the plugin comes with a set of Vue-specific rules
that can help you customize the config even further in detail.
Below is the example .eslintrc.js file from one of my recent projects.
54 Advanced Inertia
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:vue/vue3-recommended",
"prettier",
],
rules: {
"vue/multi-word-component-names": "off",
"vue/component-api-style": ["error",
["script-setup", "composition"]
],
"vue/component-name-in-template-casing": "error",
"vue/block-lang": ["error", { script: { lang: "ts" } }],
"vue/define-macros-order": ["error", {
order: ["defineProps", "defineEmits"],
}],
"vue/define-emits-declaration": ["error", "type-based"],
"vue/define-props-declaration": ["error", "type-based"],
"vue/no-undef-components": "error",
"vue/no-unused-refs": "error",
"vue/no-v-html": "off",
"no-undef": "off",
"no-unused-vars": "off",
},
ignorePatterns: ["*.d.ts"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
}
Advanced Inertia 55
I stick to the default rulesets provided by the packages above, with some
tweaks applied to my preference. This config overrides some rules specified in
the “strongly-recommended” config:
Don’t enforce component names to be always multi-word
Enforce Composition API with <script setup> short syntax in single file
components
Enforce PascalCase for the component naming style in templates
Enforce TypeScript
Enforce compiler macros order
Enforce declaring types for props and emits
Allow the use of undeclared variables
Allow unused variables
Allow v-html directive
It also instructs ESLint to ignore some file formats that don’t require to be
checked by this tool.
Running fixes
At this point, we need a script to run ESLint to report on and automatically fix
errors where possible.
Add the format script to the package.json file. The path pattern in the
example below informs ESLint to look for files within the resources directory
and its subdirectories.
{
"lint": "eslint \"resources/**\" --fix",
}
56 Advanced Inertia
Now, you can run the command npm run lint in your terminal to perform the
check. The lint command is useful for local development and running in your
CI/CD pipeline.
To run the formatter automatically every time you build a project, modify the
build script as well:
{
"build": "run-p format type-check && vite build",
"build": "run-p format lint type-check && vite build",
"lint": "eslint --fix"
}
IDE integration
To get the best developer experience from the tool, you should integrate it into
your IDE to be informed about code style violations right in the code editor.
Advanced Inertia 57
npm i -D vite-plugin-eslint
# or
yarn add -D vite-plugin-eslint
Then, register the plugin by importing it and pushing it to the plugins array in
vite.config.ts
That’s it! Now, every error ESLint discovers during the runtime will be reported
in realtime in the browser.
Advanced Inertia 59
Structuring apps
resources/
├── css
├── fonts
├── images
├── scripts
└── views
With the default Laravel structure, there’s a 99% chance you only store a few
blade files within the views directory, which only purpose is to provide a
container for virtual DOM rendered by your JS framework. Meanwhile, even
though Inertia pages (Vue components) are still JS code, their primary purpose
is to provide page views.
60 Advanced Inertia
On the other hand, you may have supporting utilities, third-party plugin
wrappers, and development helper tools (such as type declarations) that are
only connected with your views indirectly. We don’t store views near
controllers, as we don’t process code in blade views. So, it probably makes
sense to decouple actual scripts from views as well.
So, my main take is that we should treat the frontend and backend as a single
well-aligned codebase rather than two separate apps. With this approach, we
don’t consider frontend just a “JS” part — it’s an integral part of an application.
We call them monoliths for a reason.
In case you don’t like the proposed conventions and prefer sticking to Laravel’s
default structure, you are free to keep things as they are. Keep in mind that this
book provides you with an opinionated set of practices that help build
consistent codebases that are easy to extend and maintain. It’s your right to
decide what solutions apply to your problems.
Advanced Inertia 61
The cool part is that Laravel doesn’t dictate where to store your assets, and it’s
quite a simple task to customize it. Aside from renaming folders and moving
files, you only need to make slight changes to the config files.
First, we instruct Vite on where to look for an entry point and how to resolve
aliases. Apply the following changes to vite.config.ts .
Then, configure your development environment to keep the engine in sync with
the compiler. Make these changes to tsconfig.json .
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
"@/*": ["resources/*"]
}
},
}
In case you’re using Tailwind CSS with JIT mode enabled, don’t forget to make
it track your views in tailwind.config.json .
module.exports = {
content: [
"./storage/framework/views/*.php",
"./resources/views/**/*.blade.php",
"./resources/js/Pages/**/*.vue",
"./resources/views/**/*.vue",
],
}
Scripts
Here we store the core of your frontend — scripts that help the app function
and provide supporting logic to your views.
resources/scripts/
├── composables
├── plugins
├── types
├── utils
└── app.ts
We will dig deeper into the concepts of plugins , utils , and composables
later. Let’s start with moving the entry point first.
Move app.ts to resources/scripts directory.
Views
With Laravel and the framework of your choice (Vue / React / Svelte), we
usually only have a single blade entry point with a bunch of JS-powered view
components.
I prefer to store actual views next to the blade entry point. This makes sense as
we treat them equally as things that provide HTML views.
64 Advanced Inertia
resources/views/
├── components
├── layouts
├── pages
└── app.blade.php
Components
resources/views/components/
├── buttons
│ ├── button-primary.vue
│ └── button-danger.vue
├── modal.vue
├── slideover.vue
└── paginator.vue
Advanced Inertia 65
I prefer going a level down for grouping similar components, as you may have
noticed in the example above. We may still store them in the root directory, as
they are still prefixed with the type, but too many variations of a single
component may lead to bloating the directory (which is easily achievable with
such components as buttons).
Note that we only store “wrappers” for view components here. Actual partials
have a better place to live, and it’s going to be reviewed below.
So here’s the general guide on structuring components:
Every component should be an SFC (single file component)
The root components directory should only contain shell components for
actual view partials or components that are repeated several times across the
codebase
Similar components should be prefixed, and not postfixed, even though it
may not sound like proper English (ButtonDanger, AlertSuccess)
Similar components should be grouped in lower-level directories, despite
them already being prefixed
Components should not be defined globally, regardless of how strong the
temptation is. You should have the ability to jump to a component quickly from
any place it’s referenced
66 Advanced Inertia
Layouts
Even with small-sized apps, you usually have from 2 to 3 different layouts,
which include:
Landing / guest pages
Primary application layout
Authentication pages (sign up, sign in, password reset, etc.)
Each layout could be split into several smaller parts. Let’s use as an example a
basic “app shell” layout; it may consist of the following:
Sidebar
Heading
Footer
And every partial component can be composed of several even smaller partials.
So, we group each layout and its partials in separate directories to avoid a mess
of unrelated child components. And with that, we are coming up with the term
partial.
So, while higher-level components are usually wrappers for similar components
used across the app, partials are just smaller parts of a page or layout, only
intended to reduce the amount of code within a single component and simplify
the code.
Advanced Inertia 67
resources/views/layouts/
├── main
│ ├── partials
│ │ ├── heading.vue
│ │ ├── footer.vue
│ │ └── sidebar.vue
│ └── layout-main.vue
├── auth
└── landing
Pages
Structuring page components and their parts is challenging. I’ve tried to follow
different conventions, and the majority of them never worked well for me.
Most of style guides propose grouping components by context and placing
partials within a single components directory.
At first sight, this looks like a pretty simple and obvious solution:
resources/views/pages/
└── users
├── index.vue
├── edit.vue
└── show.vue
resources/views/components/
├── user-card.vue
└── user-profile-card.vue
But as mentioned before, mixing shell components and actual view partials is
not the best idea. More than that, it’s neither a good idea to put partials of
different pages in the same place.
Take the example above. Say you have an index page with a list of users, and
it has a child component whose only responsibility is to display a card with
short user info. At the same time, you also have a separate user profile page
that includes multiple partials.
Advanced Inertia 69
How would you call these components to keep the naming consistent and
intuitive and not confuse them with each other? What if you have several
partials of different pages with a similar purpose but a different context?
I group pages and their partials by the following these principles:
Pages are grouped by a context (e.g., CRUD-like one, when applicable)
Every page should be placed in a dedicated directory
Child components of a page should be placed in a partials directory next
to the parent page component
Complex partials should be divided into smaller parts, and every part should
also have its own directory
Here’s the example
resources/views/pages/users
├── create
│ └── modal.vue
├── index
│ ├── partials
│ │ ├── filters.vue
│ │ └── user-card.vue
│ └── page.vue
└── show
├── partials
│ ├── recent-events.vue
│ └── user-info.vue
└── page.vue
70 Advanced Inertia
It may seem that we overcomplicate things this way, opening the possibility of
having too many nested directories with only a few files each. But in reality, if
you optimize your components well enough and have wrappers for every similar
repeatable component, this will be be the perfect structure for your apps and
an ultra time saver for future maintenance.
Note that I call primary page components just “page” or “modal” (depending on
the type of component). You are not going to reference page components
anywhere aside from the backend, so it’s pretty safe to call them whatever you
want, assuming you already store them separately.
That’s my personal preference, as I want to avoid redundant words:
Inertia::render('users/show/show');
So, it makes sense to call them all the same way as page , modal , or
slideover , since their paths already contain the name of a view.
Inertia::render('users/show/page');
Inertia::modal('users/create/modal');
Advanced Inertia 71
Static assets
With Laravel Mix, you were probably either keeping static assets in the
public directory or had the copy hook in the config file to copy these files
from the resources directory.
With Vite, you can reference these files directly with aliases, utilizing the
original Vue way. Your IDE also treats these files as any other assets, bringing
you automatic suggestions.
So, you can store any frontend-related static assets within the resources
directory, staying confident that they are reachable from the frontend code.
resources/images/
├── logo.png
└── hero.svg
Also, you can even reference CSS files in the style section instead of
importing them from the script part:
The best part is that Vite postfixes file names with their hashes during the
build, so once you modify or replace some asset, it will be referenced with the
new name in the production build, keeping the browser cache up-to-date.
72 Advanced Inertia
Naming guidelines
Consistent naming is one of the most important parts when it comes to code
management and developer experience. You want things to be as intuitive as
possible whenever you want to add a component to your template.
These are real examples of component names from a project I jumped on
recently. I had a tough time figuring out what components I wanted to import
are called.
resources/views/components/buttons
├── base-button.vue
├── outline-btn.vue
└── upgradeBtn.vue
<BaseBtn>
<BaseButton>
<FrmInput>
<FormInput>
Multi-word components
Component names should be multi-word when it’s possible and logically
accurate. The rule shouldn’t be too strict regarding components that can’t be
prefixed within the context, such as Heading or Sidebar .
<script setup>
import Button from "@/views/components/button.vue"
import BaseButton from "@/views/components/base-button.vue"
</script>
74 Advanced Inertia
Word order
Component names should start with higher-level words and end with modifiers.
This helps us maintain consistent naming across different components and lets
IDE pre-load a list of available components when you start typing.
resources/views/components/icons
├── icon-arrow.vue
├── icon-bars.vue
└── icon-building-office.vue
<script setup>
import ButtonOutline from "@/views/components/button-outline.vue"
</script>
<template>
<ButtonOutline />
</template>
Advanced Inertia 75
Base components
Base or shell components should start with a specific prefix, such as Base .
This way, we ensure that we prevent conflicts with existing HTML elements
(e.g., BaseButton vs. Button ).
<script setup>
import BaseModal from "@/views/components/base-modal.vue"
</script>
<template>
<BaseModal>
...
</BaseModal>
</template>
76 Advanced Inertia
Composition API
Before Vue 3, Options API was the only way to create Vue components.
Developers were pretty happy with it, and its magic, and no one complained
about it.
With the recent major update, Vue introduced a brand new concept called
Composition API. First, Composition came up with an unusual, and let’s be
straight, weird for Vue developers approach to defining components. It wasn’t
clear enough what benefits the new way provides over the original one.
Previously, you would declare the component’s state inside the data()
method, and the properties were reactive by default. Props could be defined
within the props object, and dynamic getters had their place in the
computed property within the main object.
Advanced Inertia 77
<script>
import TextInput from "@/components/TextInput.vue"
export default {
props: ["title"],
components: { TextInput },
data() {
items: [],
placeholder: { title: '' }
},
methods: {
addItem() {
this.items.push(this.placeholder)
},
removeItem(index) {
this.items.splice(index, 1)
}
},
watch: {
items: (value) => {
this.$emit('value', value)
}
}
}
</script>
With the novel API, you had to define state properties in the setup() method
next to each other with no clear separation, declare variables' reactivity and
computed properties manually, and export everything to use within a template.
78 Advanced Inertia
Compare this to the example above. Does this look like a more convenient way
to build components?
<script>
import { watch, reactive } from "vue";
import TextInput from "@/components/TextInput.vue"
export default {
components: { TextInput }
props: ['title'],
emits: ['update:value'],
setup(props, { emit }) {
const placeholder = { title: '' }
return {
props,
items,
addItem,
removeItem
};
},
}
</script>
Advanced Inertia 79
So, from what I’ve seen from countless discussions on social media,
Composition API has thrown off a vast majority of developers, who preferred to
stick to the familiar original way.
To be honest, I couldn’t get the advantages of the new approach as well until
the new short <script setup> syntax was introduced. This is what I would
expect from Composition API initially. It takes no boilerplate code to start
building, has better runtime performance, and has a way more efficient
TypeScript support.
Here’s an example of how the code above would look with the short <script
setup> syntax.
<script setup>
import { watch, reactive } from "vue"
import TextInput from "@/components/TextInput.vue"
The “new” Composition API is just pure JavaScript with extra hooks and stuff.
For everyone concerned, <script setup> is the officially recommended
syntax to build single file components (what everyone should do).
So, what’s the real difference between the two APIs, and which one is the best
for you?
Separation of concerns
Options API uses several code blocks to group code logic by type, such as:
props
data (component state)
methods
mounted
etc
Many developers love that Options API enforces this strict structure and
doesn’t leave room for hesitation about organizing your code. However, when
your application becomes larger, and a component’s logic grows beyond a
certain threshold of complexity, it becomes difficult to trace the code and
manage it.
Advanced Inertia 81
The main issue with code organization is that you need to hop around different
code blocks to manage a single logical concern because you have to separate
the code that logically belongs together across multiple parts.
<script>
export default {
data() {
return {
amount: 0,
status: 'draft',
show: false
}
},
methods: {
setStatus(value) {
this.status = value
},
setAmount(value) {
this.amount = value
},
toggle() {
this.show = !this.show
}
}
}
</script>
82 Advanced Inertia
Composition API solves this limitation. Now, you can group all the related code
by its logic instead of the type.
<script setup>
const status = ref('draft')
const setStatus = (value) => status.value = value
Composition API doesn’t promote any structure, which might make it harder to
enforce any structure. In reality, I believe that components should not hold
much logic. The best thing you can do to your components is to keep a single
component responsible for a single task only.
Advanced Inertia 83
Reactivity
Previously, you could define properties within the data() method, staying
confident that they are all reactive. In Composition API, you have to manually
declare the variable’s reactivity by wrapping it into ref() or reactive()
methods.
In the example below, even though the value still changes internally, the
template never learns about updates and keeps rendering “Advanced Inertia”.
<script setup>
var title = "Advanced Inertia";
<template>
<h1>{{ title }}</h1>
</template>
84 Advanced Inertia
Once you wrap it into ref , it becomes reactive, so every change will be
reflected on both script and template parts.
<script setup>
import { ref } from "vue"
<template>
<h1>{{ title }}</h1>
</template>
Note that you have to access the variable via the value getter.
Both ref and reactive methods are pretty similar, with one exception that
ref can accept any data, while reactive only works for object types
(object, arrays etc) and is used directly without the .value property.
<script setup>
const state = reactive({ count: 10 })
state.count++;
</script>
<template>
<div>{{ title }}</div>
</template>
Advanced Inertia 85
Computed properties
<script setup>
const amount = ref(2);
const doubled = computed(() => amount.value * 2)
amount.value = 4;
</script>
<template>
<!-- outputs 8 -->
<div>{{ doubled }}</div>
</template>
86 Advanced Inertia
Like in the Options API, you can create a writable ref object with get and set
functions.
<script setup>
import { ref } from "vue"
<template>
<input v-model="internalValue" />
</template>
Advanced Inertia 87
Watching changes
With watch() , you can both track a single reactive or computed property
directly or wrap logic into a callable to watch conditional changes.
Props
<script setup>
const props = defineProps({
title: String
})
// Access props
props.title
</script>
<template>
<h1>{{ title }}</h1>
</template>
Note that you can’t access props values directly from the script section via
this . Instead, you should declare a constant that carries all the props passed.
That said, props are automatically unwrapped in templates so you can access
them directly.
Advanced Inertia 89
Emits
We use emits to trigger events and pass data up to a parent component. Unlike
Options, the Composition API requires you to declare emits explicitly.
<script setup>
import { ref } from "vue"
<template>
<input v-model="internalValue" />
</template>
The defineEmits macro returns a callable function that accepts the event’s
name as the first argument and fires an event up the component hierarchy once
triggered.
90 Advanced Inertia
TypeScript support
Typing props
Typed props take the most critical part in building a connecting layer between a
backend and a frontend in Inertia-powered apps. We need to know for sure
what data is passed down to the page, and the types help us define its shape.
Even though Composition API lets you use either runtime or type declaration, I
recommend sticking to the pure-type syntax with literal type arguments.
Typing emits
You can also type emit functions, which helps your IDE suggest defined events
and the payload type required.
Advanced Inertia 93
Mixins
Even though Vue 3 still supports mixins due to migration reasons, Composition
API doesn’t provide such a feature. Honestly, we all should have forgotten them
earlier because of several common drawbacks:
Unclear source of properties: when using many mixins, it becomes unclear
which instance property is injected by which mixin, making it difficult to trace
the implementation and understand the component's behavior. This is also why
we recommend using the refs + destructure pattern for composables: it makes
the property source clear in consuming components.
Namespace collisions: multiple mixins from different authors can potentially
register the same property keys, causing namespace collisions. With
composables, you can rename the destructured variables if there are conflicting
keys from different composables.
Implicit cross-mixin communication: multiple mixins that need to interact
with one another have to rely on shared property keys, making them implicitly
coupled. With composables, values returned from one composable can be
passed into another as arguments, just like normal functions.
Mixins could be easily replaced by a concept of composables, which will be
reviewed further.
94 Advanced Inertia
Concepts
Singletons
One of the popular ways to share common pieces of data across multiple
JavaScript modules is still heavy using the window object.
However, there are several common reasons global variables are considered
antipattern:
It can be accessed from any other modules
It can lead to name collisions with other libraries or methods
It misses type-safety and direct IDE support
In the end, you can never be sure that the object you refer to is instantiated and
even present on the window object.
96 Advanced Inertia
Take this example from a basic Laravel setup. It creates the Echo data object
on the window object that carries an initialized instance of laravel-echo .
We can access this object anywhere as long as the module containing this
definition is imported somewhere in the app.
Echo.private("chat")
.listen(/* ... */)
Everything you need to make some class a singleton is to export its initializer.
Now, you can import it from any module or component, being confident that the
instance is only resolved once and sharing its properties with all references.
echo.private("chat")
.listen("MessageSent", (event) => {
messages.push(event.message)
})
Please note that Echo is built using pure JavaScript and doesn’t have
TypeScript typings by default. For the example above, you may need to install
typings for the package.
Similar to classes, functions can also be singletons. The best use case of
singleton functions for Vue.js developers is reactive composables.
98 Advanced Inertia
Composables
return { isDark }
}
Advanced Inertia 99
Now, you can reuse this piece of code, staying confident that the state of this
function is the same for any component.
<script setup>
import { useMode } from "@/composables/use-mode"
mode.isDark = true
</script>
Aside from just carrying reactive variables, composables may implement any
state-related methods.
Utils
Unlike composables, utils are reusable stateless functions that take some input
and immediately return an expected result.
You are probably already familiar with the concept if you’ve ever used such
utility libraries as lodash or written any helpers. Basically, utility is a helper
method that doesn’t involve state management.
A basic example of a utility is a date formatter. Laravel returns datetime
properties’ values in the default MySQL format YYYY-MM-DD hh:mm:ss , and
we often need to display it in a user-friendly format, relative or absolute.
We can already use such a library as moment.js in Vue templates directly.
<script setup>
import moment from "moment"
</script>
<template>
<div>
{{ moment().format('MMMM Do YYYY, h:mm:ss a') }}
</div>
</template>
Advanced Inertia 101
Taking that dates are often displayed in a similar format across the app, you
may end up writing repeating code to get similar results. So, it makes sense to
extract such logic into a wrapper around moment with the default format
defined.
This way, we get a neat helper reusable across multiple components, getting rid
of boilerplate code and making sure that dates are displayed in the same
default format if you don’t use a custom one.
<script setup>
import { format } from "@/utils/format"
</script>
<template>
<td>{{ format(user.created_at) }}</td>
<!-- or -->
<td>{{ format(user.created_at, 'M D Y') }}</td>
</template>
102 Advanced Inertia
Plugins
Let’s assume we set flash messages via the default Laravel session helper
method.
Using this approach, we also need to share the flash message on each request
using the Inertia middleware.
For simple plugins, I stick to the shorter approach, which lets you write a bit
less boilerplate code.
npm i vue-toastification
# or
yarn add vue-toastification
Advanced Inertia 105
Now, let’s define some logic of the plugin. In this example, we don’t track
props directly, as the flash property could be missing on partial requests
and could lose reactivity. Instead, let the plugin listen to the finish event and
display a toast when a message appears.
if (notification) {
toast(notification)
}
})
}
106 Advanced Inertia
createInertiaApp({
resolve: (name) => importPageComponent(
name,
import.meta.glob("../views/pages/**/*.vue")
),
setup({ el, app, props, plugin }) {
createApp({ render: () => h(app, props) })
.use(plugin)
.use(notifications)
.mount(el)
},
})
Advanced Inertia 107
Depending on the notification library you’re using, you may also need to register
it as well:
createInertiaApp({
resolve: (name) => importPageComponent(
name,
import.meta.glob("../views/pages/**/*.vue")
),
setup({ el, app, props, plugin }) {
createApp({ render: () => h(app, props) })
.use(plugin)
.use(notifications)
.use(Toast)
.mount(el)
},
})
That’s it! Now, the notifications method will be called on the app’s
initialization, enabling your plugin to work separately from the frontend app’s
logic. Once a success message appears, the plugin will fire a nice toast
notification.
108 Advanced Inertia
Let’s get through a quick example from the official Inertia.js demo application
Ping CRM. That’s the code I keep seeing on GitHub, Twitter threads, and blog
posts. It’s good if it works for you, but I want to share my views on improving
the developer experience.
<script>
export default {
props: {
users: Array,
}
}
</script>
<template>
<tr v-for="user in users" :key="user.id">
<td>
<Link :href="`/users/${user.id}/edit`">
{{ user.name }}
</Link>
</td>
</tr>
</template>
That said, even though it works well from the backend perspective, you can
never be sure what data you’re dealing with on the frontend without referring to
the original data source. At this point, you know nothing about the shape of the
data your frontend receives. I mean, you know it in this exact moment when the
feature is still fresh in your mind, but as time passes, you forget such tiny
details and have to be jumping back and forth over definitions.
Take the example above. You get back to the code in a month, trying to add a
block with a user’s avatar. How did you call that property — avatar_url ,
avatar , or maybe image_url ? Jumping to the controller...
That’s a pretty raw example, but this happened to me countless times. Untyped
properties/objects/variables are a quick way to prototype stuff, but it leads to a
maintenance burden later. Your IDE only knows that you are referring to an
array of anything , and can’t help you with a hint.
Thanks to our great community, there are existing tools to help us seamlessly
glue two parts together into a single perfectly aligned codebase.
Advanced Inertia 111
Laravel Data
Laravel Data is a package by Spatie which helps you build rich data objects for
different purposes, replacing the basic functionality of:
DTO (Data Transfer Objects)
Requests
Resources
The main difference between these data objects and built-in Laravel features is
that you can strictly type every piece of data coming from or going to a
frontend, which is crucial when you build an app with a rich dynamic frontend.
What’s essential for the frontend is that it lets us generate TypeScript interfaces
via the TypeScript Transformer package automatically. You will not need to
write a single interface manually.
Long story short, the package is so good that I hope that one day we’ll see it as
an official part of the Laravel core.
Laravel Data is an essential package to connect two parts of the app together.
If you aren’t using it already, install the package via composer:
TypeScript Transformer
return [
'collectors' => [
DefaultCollector::class,
DataTypeScriptCollector::class,
],
'transformers' => [
SpatieStateTransformer::class,
SpatieEnumTransformer::class,
DtoTransformer::class,
EnumTransformer::class,
],
];
Advanced Inertia 113
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
enum Employment: string
{
case OnSite = 'on-site';
case Remote = 'remote';
case Hybrid = 'hybrid';
}
return [
'output_file' => resource_path('scripts/types/generated.d.ts'),
];
Resources
type UserData = {
id: number;
name: string;
email: string;
}
class UserController
{
public function show(User $user)
{
return inertia('users/show/page', [
'user' => UserData::from($user),
]);
}
}
116 Advanced Inertia
Now, you can use generated interfaces in Vue components, getting automatic
suggestions and type-safety your IDE and CLI tools can provide now.
So, whenever you try to access a property user in the example above, your
IDE will suggest a list of available properties, and both IDE and the compiler will
warn you when you try to access a non-existing one.
Advanced Inertia 117
Requests
With Laravel Form Requests, you define what data you expect to receive from
the frontend or API calls and apply validation rules to the incoming data.
For example, let’s say you send a request to the backend with the following
data:
{
"name" : "",
"feed": "https://feeds.transistor.fm/hammerstone",
"last_updated_at" : "2022-07-17 00:00:00"
}
Once the data passes validation and authorization and gets to a controller, it
becomes an arbitrary data set. You don’t precisely know what shape the data
has until you jump to the request’s definition and check it.
When you’re dealing with simple apps without complex data manipulations, you
can usually map the request to a new or existing database record:
In case the database table definition differs from the request, or you need to do
some extra manipulations in between, you may need to access some
parameters directly:
if ($request->boolean('fetch')) {
ProcessPodcast::dispatch($podcast);
}
}
Advanced Inertia 119
Note that even if you set rules that imply the type of data coming in ( date in
the example above), form requests don’t automatically cast data to the desired
format. Instead, you need to explicitly call the method of the Request instance,
specifying what kind of data you are expecting to get. Still, you’re only limited to
boolean , date and collection types.
Like resources, Data objects can act like requests, letting you define properties
explicitly. Aside from transforming data to a required format, Laravel Data can
automatically validate incoming payload by inferring rules from property types.
Data objects let you define validation rules using the attribute notation.
use Spatie\LaravelData\Attributes\Validation;
From the controller’s perspective, Data Requests are utilized the same way as
Form Requests, meaning that you can inject them into a controller’s method,
and it will automatically perform the validation.
122 Advanced Inertia
So, when the request touches a controller, you can access it just like any class
instance with explicitly defined properties while having perfect IDE
autocompletion and getting data in the desired format.
Aside from that, we can equally benefit from Data Requests from both frontend
and backend development perspectives. Just like we type props going from a
backend, we can type data coming from a frontend.
Here’s what a TypeScript definition automatically generated by TypeScript
Transformer may look like:
So, when you create a form instance or access it from the template, your IDE
knows what properties it does have and what type they should be. During the
compilation step, the type checker also analyzes the code and notifies you if
something is wrong, dramatically decreasing the chances of making a mistake.
124 Advanced Inertia
The only downside of the described approach is that you still have to run
typescript:transform manually each time you make changes to your data
classes.
Luckily, this process could be automated with the help of the Vite plugin vite-
watch-run . This package can run any commands on specific files’ changes.
npm i -D vite-plugin-watch
Then, modify your vite.config.ts configuration file and set up the watcher:
This way, when Vite detects changes in tracked files, it will automatically run
the specified command, keeping your TypeScript interfaces up-to-date.
126 Advanced Inertia
Wrapping it up
Pagination
Dealing with paginated datasets is no more complicated than with plain data.
Laravel Data can handle paginated collections without any extra effort required.
return inertia('users/index/page', [
'users' => UserData::collection($users),
]);
}
The collection(...) method wraps the payload in the same kind of object
basic Laravel resources usually produce. You can access records on the
frontend using the data property.
128 Advanced Inertia
On the frontend
npm i momentum-paginator
# or
yarn add momentum-paginator
Import the package, and init the paginator by passing the paginated data you
receive from the backend.
The page object contains some extra info about links, allowing you to
customize the look of buttons depending on their state.
When implementing actual pagination on the frontend, I recommend extracting
it into an abstract component so that it can be reused on any page with any
dataset provided.
130 Advanced Inertia
<template>
<div>
<div>
<component
v-for="page in pages"
:is="page.isActive ? 'a' : 'span'"
:href="page.url"
:class="{
'text-gray-600': page.isCurrent,
'text-gray-400': page.isActive,
'hover:text-black': !page.isCurrent && page.isActive,
}"
>
{{ page.label }}
</component>
</div>
<div>
Showing {{ from }} to {{ to }} of {{ total }} results
</div>
</div>
</template>
Advanced Inertia 131
Authorizations
Policies
The most common way to define authorization rules for Eloquent models is
Policies.
Here’s the example policy from one of my recent projects:
class PodcastPolicy
{
public function view(User $user, Podcast $podcast)
{
return $podcast->user()->is($user);
}
Data Resources
Momentum Lock is a Laravel package built on top of Laravel Data, which adds
extra functionality to Data objects, letting you append authorizations to
responses and handle them on the frontend level.
First, install the composer package:
use Momentum\Lock\Data\DataResource;
You can either specify the list of abilities manually or let the package resolve
them from the corresponding policy class.
Momentum Lock doesn’t require any additional steps, and you can pass data as
you already do without any extra changes required.
{
"id": 16,
"name": "Full Stack Radio",
"permissions": {
"viewAny": true,
"view": false,
"create": true,
}
}
136 Advanced Inertia
TypeScript definitions
return [
'collectors' => [
Momentum\Lock\TypeScript\DataResourceCollector::class,
DefaultCollector::class,
DataTypeScriptCollector::class,
],
];
Note that it should be put first in the list to get priority over the default
DataTypeScriptCollector .
Advanced Inertia 137
Momentum Lock also provides a frontend helper method can . This function
checks whether the required permission is set to true on the passed object.
Install the frontend package.
npm i momentum-lock
# or
yarn add momentum-lock
With this method, once you pass a DataResource instance as the first
argument, the IDE will give you a list of available options for the ability type
and warn you if you use a non-existing check.
<template>
<div v-for="podcast in podcasts" :key="podcast.id">
<a
v-if="can(podcast, 'edit')"
:href="route('podcasts.edit', podcast)">
Edit
</a>
</div>
</template>
138 Advanced Inertia
Routing
Momentum Trail
The package Momentum Trail is built on top of Ziggy and provides a set of
type-safe route helpers for the frontend, letting your IDE understand the
arguments passed to route and current methods.
First, install the package using composer .
Set the paths according to your directory structure. If you are following the
setup described in this guide, feel free to keep default values.****
return [
'output' => [
'routes' => resource_path('scripts/routes/routes.json'),
'typescript' => resource_path('scripts/types/routes.d.ts'),
],
];
140 Advanced Inertia
npm i momentum-trail
# or
yarn add momentum-trail
Register your routes on the frontend. Import the generated JSON definition and
pass it to the defineRoutes method within the entry point ( app.ts ).
defineRoutes(routes);
Alternatively, you can register routes using a Vue 3 plugin coming as a part of
the package.
createInertiaApp({
setup({ el, app, props, plugin }) {
createApp({ render: () => h(app, props) })
.use(trail, { routes })
}
})
Advanced Inertia 141
Import the helper in your .vue or .ts files and enjoy autocompletion and
type-safety for both route and current methods.
<template>
<a
:class="[
current('users.*') ? 'text-black' : 'text-gray-600'
]"
:href="route('users.index')">
Users
</a>
</template>
142 Advanced Inertia
Once you start typing route , the IDE will suggest a list of available routes and
the payload required for the route.
The route helper function works like Laravel's — you can pass the name of
one of your routes and the parameters you want to pass to the route, and it will
return a URL.
route("jobs.index")
route("jobs.show", 1)
route("jobs.show", [1])
route("jobs.show", { job: 1 })
The current method lets you check if the current URL matches with the route
and can accept both full route names with arguments and wildcard routes.
current("jobs.*")
current("jobs.show")
current("jobs.show", 1)
Advanced Inertia 143
Auto-generation
The package works best with vite-plugin-watch plugin. You can set up the
watcher to run the command on every file change, so whenever you change the
contents of any .php file within the routes directory, the package will
generate new definitions for the frontend.
Global state
Quite often, you need to have access to common data shared across different
pages. As a good example, this could be such data as:
Currently authenticated user
User’s team
Notifications
Flash messages
The official Inertia documentation proposes merging shared data from
HandleInertiaRequests middleware directly so that it will be included in
every response.
This default approach works well but doesn’t give us any confidence in data
shape from the frontend development perspective.
As we already utilize versatile data objects, we can create one to represent the
shape of data going to frontend and get an auto-generated TypeScript
definition via TypeScript Transformer.
return array_merge(
parent::share($request),
$state->toArray()
);
}
146 Advanced Inertia
If you utilize Inertia’s partial reloads, you can make SharedData accept
LazyProps or closures.
Unfortunately, Laravel Data doesn’t let you use union types for properties
that are typed as Data objects, so you might like to annotate the property
with the #[TypeScriptType] attribute for TypeScript Transformer to
understand its type.
use Spatie\TypeScriptTransformer\Attributes\TypeScriptType;
When Inertia performs a partial visit, it will determine which data is required,
and only then will it evaluate the closure.
return array_merge(
parent::share($request),
$state->toArray()
);
}
}
On the frontend, global page props can also be typed to reflect the shape of
data it may contain.
As we usually share the same data across pages, it makes sense to type
usePage once and have it properly typed everywhere to prevent writing
similar code repeatedly. Luckily, we can extend any type, class, or method of
any third-party package installed.
Create a file page.d.ts within the resources/scripts/types directory:
import "@inertiajs/vue3"
import { Page } from "@inertiajs/core"
This way, we inform the TypeScript engine that the usePage method’s return
type should infer properties from our newly created SharedData type.
So, now you can use the usePage hook as you already do, with the bonus of
free IDE suggestions and type safety:
Advanced Inertia 149
Flash notifications
Flash messages are an important part of Laravel applications that let you pass
one-time notifications stored in the session only for the next request. They are
usually shared as global props using with session helper on redirects.
We can use the global state helper class SharedData to carry the contents of
flash messages and bring type-safety to both the frontend and backend.
First, create a data object that represents a notification.
If you prefer avoiding arbitrary values, you should create an enum class that
reflects a type of notification.
Make sure these values match the notification types your frontend library
accepts. Usually, these are:
success
error
info
warning
#[TypeScript]
enum NotificationType: string
{
case Success = 'success';
case Error = 'error';
}
This way, with the help of TypeScript Transformer, you can get typed
notifications on the frontend.
// string
page.props.notification .body
// success | error
page.props.notification .type
Advanced Inertia 151
In Laravel, you can register custom macros within service providers. I prefer
having a dedicated provider for anything Inertia-related.
Create a service provider using the following command:
return [
'providers' => [
App\Providers\InertiaServiceProvider::class,
],
];
Advanced Inertia 153
Now, you can register the macro, which accepts the notification type and its
contents and shares the value with the session.
Starting this point, you can trigger flash notifications using the flash method.
You can also create dedicated methods for the most common notification
types.
If you use Laravel IDE Helper Generator, it will automatically generate helpers
so that your IDE will understand these newly created methods.
Otherwise, you can type these methods manually. Create a file ide-
helpers.php in the root directory of a project with the contents:
namespace Illuminate\Http {
/**
* @method static success(string $body)
* @method static error(string $body)
*/
class RedirectResponse
{
//
}
}
These helpers will not affect the runtime but will help the IDE understand
dynamically created macros.
156 Advanced Inertia
On the frontend, you can use the plugin described in the Plugins section with a
slight change. Now, as we provide the type of notification along with its
contents, we can pass it to the toast method as well.
if (notification) {
toast(notification)
toast(notification.body, { type: notification.type })
}
})
}
Cleaning things up
I’m not much a fan of any non-explicitly defined stuff, whether it’s window
objects, mixins, or globally registered components, and I try to avoid them at all
costs.
Still, there are situations when you can clean up your code, getting rid of clutter
without compromising on DX.
Unified plugins
Unplugin is a concept of Vite plugins that provide a transformation layer that
can modify your code during the compilation process before it goes to a
bundler.
This enables developers to create tools simplifying the development process. In
short, unplugins work similarly to default Vue.js compiler macros
defineProps and defineEmits — they are only compiled into a "real" code
when <script setup> is processed while having perfect IDE support.
158 Advanced Inertia
Auto imports
The plugin lets you auto-import both local and third-party methods and
classes.
First, install the package.
npm i -D unplugin-auto-import
# or
yarn add unplugin-auto-import
Advanced Inertia 159
Then, register the plugin. Make sure to specify the correct path for TypeScript
type definitions.
In this particular case, it may not seem that impressive since we only got rid of
a few imports. Still, when you are building a heavy Vue.js app that uses
common helper methods and such libraries as Inertia.js, VueUse, or packages
from the Momentum ecosystem, you may end up saving lots of useless imports
in each component.
While having first-class support of such common libraries as Vue.js, React, and
VueUse, the package lets us auto-import any methods from any third-party
packages installed.
The example below demonstrates how to register common methods from
several packages. So, whenever you use a method listed in the config, the
plugin will automatically generate a TypeScript definition for the alias and
import the source on the compilation step.
Methods like useForm , route , and can could be used in the vast majority
of page components, making you import them again and again over different
components, so it makes sense to register them globally.
<template>
<button @click="destroy" v-if="can(company, 'delete')">
Delete
</button>
</template>
162 Advanced Inertia
Global components
npm i -D unplugin-vue-components
# or
yarn add unplugin-vue-components
Advanced Inertia 163
Now register the plugin. Specify the directory with your common components.
Set a separate file path for the type definitions, as they will be overwritten
every time you re-compile the frontend.
This way, Vite will automatically register your local components, letting you use
them without imports. The great part is that your IDE keeps understanding
them as natural imports, keeping the support of required attributes and slots.
<template>
<InputText v-model="name" />
</template>
164 Advanced Inertia
The unplugin also lets you register components from any installed third-party
packages.
In this example, we create a resolver for Link and Head components.
Whenever Vite finds an undefined component, it tries to find a resolver that
should handle it.
In this case, when Vite finds a component with the tag Link or Head , it will
automatically import it from the @inertiajs/vue3 package.
if (components.includes(name)) {
return { name, from: "@inertiajs/vue3" }
}
},
],
}),
],
})
Advanced Inertia 165
Conclusion
<template>
<LayoutGuest>
<Head title="Set up your company" />
<InputText
id="name"
v-model="form.name"
type="text"
class="mt-1 block"
required
autofocus
autocomplete="name" />
Afterwords
That’s it!
I hope you enjoyed the read and that some ideas and concepts from this book
will fit your projects well.
This whole guide reflects my views on improving the developer experience for
building monolith apps consisting of two separate codebases.
Inertia has been a game changer for me, and I can only see a bright future for
the whole ecosystem. The library is in active development right now, so this
book might be revised in the future to keep it up to date.
168 Advanced Inertia
The most ideas I’ve described are not my own. I’ve read countless posts and
courses and taken inspiration from numerous great developers from the
community, including but not limited to:
Jonathan Reinink (@reinink)
Marcel Pociot (@marcelpociot)
Samuel Štancl (@samuelstancl)
Lars Klopstra (@LarsKlopstra)
Enzo Innocenzi (@enzoinnocenzi)
Alex Garrett-Smith (@alexjgarrett)
Aaron Francis (@aarondfrancis)
Brent Roose (@brendt_gd)
Steve Bauman (@ste_bau)
Freek Van der Herten (@freekmurze)
Yaz Jallad (@ninjaparade)
Sebastian De Deyne (@sebdedeyne)
and others.
Thank you!
— Boris Lepikhin, 2023