Uploaded by gritty.gate5369

advanced-inertia

advertisement
© 2023 Boris Lepikhin
Table of contents
Foreword
Introduction
Dealing with types
Why TypeScript?
Types and stuff
Laravel and Vue.js
VS Code Support
The Workflow
VS Code
Vite
Prettier
ESLint
Structuring apps
Switching the structure
Naming guidelines
Composition API
Separation of concerns
Reactivity
Computed properties
Watching changes
Props
Emits
TypeScript support
Mixins
The HOT take
6
7
10
11
15
28
34
35
35
37
38
46
59
61
72
76
80
83
85
87
88
89
90
93
94
Concepts
Singletons
Composables
Utils
Plugins
Working with data
Laravel Data
TypeScript Transformer
Resources
Requests
Automating the generation
Wrapping it up
Pagination
On the frontend
Authorizations
Policies
Data Resources
TypeScript definitions
Down the frontend
Routing
Momentum Trail
Auto-generation
Global state
Flash notifications
Handling on the frontend
Cleaning things up
Auto imports
Global components
Conclusion
Afterwords
95
95
98
100
102
108
111
112
114
117
124
126
127
128
131
132
133
136
137
138
139
143
144
149
156
157
158
162
165
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 endof-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
Why does this book even exist?
Full-stack or primarily backend developers tend to treat the front end of an
application as separated or disconnected from the core. For some reason, the
JS part is still often considered a supporting part for the backend, even though
it’s the only thing the end user should care about.
I don’t like writing code as much as I love seeing the result in action soon, so I
never cared much about the frontend code quality before. You know what I’m
saying, right? We prototype things quickly and put them into production soon.
But once you start jumping on larger projects, the quality and consistency of
code might pay itself off sooner than you would expect.
This guide covers both frontend and backend perspectives, from resolving
common misconceptions about frontend-driven development and structuring
dynamic applications to creating a snappy workflow and building complex apps
that are easy to maintain for years to come.
Advanced Inertia
9
Bringing two worlds together
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 autocompletion.
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
Dealing with types
TypeScript used to scare the hell out of me. Still does.
A growing number of PHP and, more specifically, Laravel developers have
started to write more strictly typed code. In contrast, most full-stack
developers still ignore the same practices in their frontend code. One reason is
that TypeScript is considered an excessive way to write frontend components.
The main misunderstanding is that TypeScript is too complex for primarily
backend developers and only bloats code with some weird extra stuff without
providing any real-world value.
In reality, TypeScript does not force you to declare types or modify any code.
Here is the critical part: TypeScript is a JavaScript superset that enables you to
add things on top, but any valid JS is also valid TS.
The real-world impact is that you can rename a file from .js to .ts and
gradually add types or start using types in new files. Your codebase does not
have to reach 100% type coverage ever. You can use TypeScript to the extent
you choose.
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.
11
12
Advanced Inertia
Does this snippet below somehow encourage you to start learning TypeScript? I
doubt.
export type ParseQueryString<
I, Query extends Record<string, any[]> = {}
> = string extends I
? Record<string, any>
: I extends ''
? {
[P in keyof Query]: Query[P]['length'] extends 1
? Query[P][0]
: Query[P]
}
: I extends `${infer K}=${infer V}&${infer Rest}`
? ParseQueryString<Rest, MergeQuery<K, V, Query>>
: I extends `${infer K}&${infer Rest}`
? ParseQueryString<Rest, MergeQuery<K, true, Query>>
: I extends `${infer K}=${infer V}`
? ParseQueryString<'', MergeQuery<K, V, Query>>
: I extends `${infer K}`
? ParseQueryString<'', MergeQuery<K, true, Query>>
: []
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
In TypeScript, this issue is easily solvable by assigning a type to an argument.
So, whenever you want to access the method, your IDE will tell you what
arguments it does accept and what result is returned.
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
Types and stuff
Let’s dig a bit into the types theory.
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.
// both IDE and the interpreter will know
// that `$product` has the type `Product`
$product = CreateProduct::execute($category, $data);
Advanced Inertia
17
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 .
For example, the code below is still a valid code:
function toggle(bool $value)
{
//
}
$value = 'on';
// `$value` will be interpreted as `true`
show($value);
18
Advanced Inertia
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);
function toggle(bool $value)
{
// From this point, PHP will treat `$value` as an integer
$value = $value * 2;
}
Advanced Inertia
19
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.
var show: boolean = true;
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
// The variable will always be a `string`
var title: string = 'Advanced Inertia';
Advanced Inertia
21
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.
function setStatus(status: string|number) {
//
}
In TypeScript, you can also declare new types. To implement the type alias, use
the type keyword to create a new type.
type Status = string | number;
function setStatus(status: Status) {
//
}
22
Advanced Inertia
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;
}
// At this point, we don’t exactly know
// what data should be passed
publish(...)
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
}
const user: User = {
id: 1,
email: 'user@example.com',
image: null,
}
Advanced Inertia
23
In rare cases, when the object is only used once, you can even omit the
declaration and define the type anonymously:
function publish(payload: { title: string }) {
//
}
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
}
function publish(tweet: Tweet) {
//
}
// Object types are abstract,
// so you don’t have to instantiate them
publish({
body: "Hey, that's my first tweet."
})
24
Advanced Inertia
In modern TypeScript, interfaces are almost identical to object types, with
subtle differences.
interface Tweet
{
body: string
}
const tweet: Tweet = {
body: "Hey, that's my second tweet",
}
Arrays
In JavaScript, you can never be sure what data an array contains. TypeScript
lets you type the shape of arrays as well.
// Starting here, TypeScript knows that
// `tabs` should carry an array of strings
var tabs: string[];
tabs = ["All", "Published", "Drafts"];
Advanced Inertia
You can apply any existing type to hint array elements.
type Tweet = {
body: string
}
function publishAll(tweets: Tweet[]) {
tweets.forEach((tweet) => {
// TypeScript knows that `tweet` represents an object
// with the type `Tweet`
// and helps you with suggestions and type-checking
})
}
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.
type Token = { content: string }
type Line = Array<Token>
25
26
Advanced Inertia
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.
var value: any = null;
// 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
}
const paginator: Paginator<Tweet>
// TypeScript infers `data` type from the type specified
// so it knows what shape the object has
paginator.data[0].body
Dealing with typed data with real-world use cases is described within the
Working with data chapter.
28
Advanced Inertia
Laravel and Vue.js
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.
Install the dependencies
Let’s start with installing the TypeScript compiler.
npm i -D typescript
Advanced Inertia
29
Set up the TypeScript config
TypeScript compiler needs a configuration file containing the required options.
Create a file tsconfig.json within the root folder of your project:
{
"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
With this configuration, we instruct TypeScript to:
Build JavaScript to the latest available version supported
Enable strict type-checking options
Register special types for Vite helpers (import.meta)
Allow regular JavaScript syntax — this helps you prevent getting exceptions
in case you don’t start from scratch but gradually refactor a project into
TypeScript
Register the @/ alias for assets (scripts, styles, etc.)
Process only .d.ts , .ts , and .vue files
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.
First, change the main entry point file’s extension to .ts :
resources/js/app.js
resources/js/app.ts
Advanced Inertia
31
Instruct Vite to use the renamed entry point.
import { defineConfig } from "vite"
import laravel from "laravel-vite-plugin"
import vue from "@vitejs/plugin-vue"
export default defineConfig({
plugins: [
laravel({
input: "resources/js/app.js",
input: "resources/js/app.ts",
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
})
32
Advanced Inertia
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.
<script lang="ts" setup>
...
</script>
<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.
is a Vue-specific wrapper around tsc — a utility for command line
type checking and type declaration generation.
vue-tsc
npm i -D vue-tsc
Advanced Inertia
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.
33
34
Advanced Inertia
VS Code Support
This section is only applicable to VS Code users.
Even though VS Code already has a built-in TypeScript engine, Volar (Vue.js
extension) brings another TS service instance patched with Vue-specific
support. To make Volar understand your plain .ts files and get them Vue.js
features support, you need to install the official extension TypeScript Vue
Plugin.
While this basic setup will work, you will have two TypeScript engine instances
running simultaneously, which may lead to performance issues in larger
projects.
To improve performance and prevent possible conflicts between two services,
Volar provides a feature called “Takeover Mode.”
To enable Takeover Mode, you need to disable VS Code’s built-in TS language
service in your project’s workspace only by following these steps:
1. In your project workspace, bring up the command palette with Ctrl +
Shift + P (macOS: Cmd + Shift + P ).
2. Type built and select “Extensions: Show Built-in Extensions.”
3. Type typescript in the extension search box (do not remove @builtin
prefix).
4. Click the little gear icon of “TypeScript and JavaScript Language Features”
and select “Disable (Workspace).”
5. Reload the workspace. Takeover mode will be enabled when you open a Vue
or TS file.
Advanced Inertia
35
The Workflow
Every project starts with a workflow. A well-configured workflow is key to
building apps with perfectly aligned codebases.
We are not going to talk about tweaking layouts and setting up shortcuts, as
these things are entirely up to personal preferences. Instead, let’s walk through
a basic frontend development tooling I consider the perfect one.
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.
Vue Language Features
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
Vite
Vite is a modern performant replacement to Webpack. If you follow the news
and use Laravel 9.0+, you should already be aware of Vite.
Vite is a modern frontend build tool that provides an extremely fast
development environment and bundles your code for production. When
building applications with Laravel, you will typically use Vite to bundle
your application's CSS and JavaScript files into production-ready assets.
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.
37
38
Advanced Inertia
Prettier
Prettier is an opinionated code formatter. It enforces a consistent style by
parsing your code and re-printing it with its own rules that take the
maximum line length into account, wrapping code when necessary.
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
Configure your code style
With the clean installation, Prettier uses a default set of format options, but you
are free to customize any rule it supports.
40
Advanced Inertia
Create a configuration file .prettierrc in the root directory of your project.
The basic setup is already sufficient enough for most of the cases, but you can
tweak it to your taste:
{
"semi": false,
"bracketSameLine": true,
"printWidth": 120,
"singleAttributePerLine": true
}
This config applies the following rules to your code:
Don’t print semicolons at the ends of statements. It also removes semicolons
if presented
Put the > of a multi-line HTML element at the end of the last line instead of
being alone on the following line
Set the line length limit to wrap on
Enforce a single HTML attribute per line
I would strongly recommend keeping this file in your git repository so that every
member of your team would have the same configuration.
Running fixes
Add the format script to the package.json file. The path pattern in the
example below informs Prettier to look for .js , .ts , .vue and css files
within the resources directory and subdirectories. Feel free to modify it in
accordance with the file paths and types you work with.
Advanced Inertia
41
{
"build": "vite build",
"format": "prettier \"resources/**/*.{js,ts,vue}\" --write"
}
Now, you can run the command npm
formatting.
run format
in your terminal to perform
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",
}
}
The package provides two CLI commands:
run-s — Run commands sequentially
run-p — Run commands in parallel
While I prefer compiling frontend at the last step, it makes sense to perform
checks in parallel so that the command stops the execution once some error
appears.
Advanced Inertia
43
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:
<div class="text-white flex px-3 rounded-lg text-sm py-1.5">
<div class="flex rounded-lg px-3 py-1.5 text-sm text-white">
<!-- ... -->
</div>
Prettier automatically discovers all installed plugins, so everything you need to
plug the Tailwind CSS support is to install prettier-plugin-tailwindcss
as a dev dependency.
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-todebug runtime errors for anyone in your team.
Advanced Inertia
47
Unlike Prettier, ESLint is highly customizable, and it doesn’t come with preconfigured 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 prepopulated options depending on your preferences.
npx eslint --init
# or
yarn run eslint --init
The output config file should look like this:
module.exports = {
extends: ["eslint:recommended"],
rules: {}
}
Install Prettier config
Sometimes, the linter might conflict with Prettier when your ESLint config
contains stylistic rules along with code quality rules. When working with both
tools, you may need to disable rules already being taken care of by Prettier.
Advanced Inertia
49
Install the Prettier’s official ESLint config:
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: {}
}
Install TypeScript plugin
By default, ESLint’s default JavaScript parser cannot parse TypeScript syntax,
and its rules don’t natively have access to TypeScript's type information.
Install @typescript-eslint/parser and eslint/eslint-plugin as dev
dependencies.
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
Set @typescript-eslint/parser as the additional parser in the parserOptions
section of your .eslintrc.js , so that ESLint could properly parse TypeScript
syntax.
module.exports = {
extends: ["eslint:recommended", "prettier"],
rules: {},
parserOptions: {
parser: "@typescript-eslint/parser",
}
}
TypeScript ESLint comes with default rulesets to provide a comprehensive base
config for you, with the intention that you add the config and it gives you an
opinionated setup:
"eslint-recommended” — Disables core ESLint rules that are already
checked by the TypeScript compiler and enables rules that promote using the
more modern constructs TypeScript allows for.
"recommended” — Recommended rules for code correctness that you can
drop in without additional configuration. These rules are those whose reports
are almost always for a bad practice and/or likely bug.
"recommended-requiring-type-checking” — Additional recommended rules
that require type information.
"strict” — Additional strict rules that can also catch bugs but are more
opinionated than recommended rules.
Advanced Inertia
51
I would certainly recommend starting with eslint-recommended and adding
more strict configs once you are comfortable enough with the current level of
strictness.
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier"
],
rules: {},
parserOptions: {
parser: "@typescript-eslint/parser",
},
}
Install Vue.js plugin
Vue.js has an official plugin for ESLint. The plugin lets check and fix both
<template> and <script> sections of Vue components and helps find Vuespecific syntax errors and violations of the Vue.js Style Guide.
Start with installing eslint-plugin-vue as a dev dependency:
npm i -D eslint-plugin-vue
# or
yarn add -D eslint-plugin-vue
52
Advanced Inertia
This plugin requires vue-eslint-parser to parse .vue files. Set it as the
default parser:
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
ESLint has first-class supported official extensions along with communitymaintained ones for the majority of popular IDEs and code editors, including:
WebStorm / PhpStorm — pre-installed
Visual Studio Code — official extension
Sublime Text 3 — community extension
Vim — community extensions ALE and Syntastic
Similarly to Prettier, once installed, the extension automatically discovers your
configuration file, so it doesn’t require any additional steps to configure it.
Vite plugin
For most cases, the proposed setup would already work well. However, if you
want to be informed about errors during the development runtime, you can
install an official plugin for Vite.
With this plugin, once a rule violation happens, Vite will display a pop-up over
your application listing the issues, just like it already does with regular syntax
errors.
57
58
Advanced Inertia
Install vite-plugin-eslint as a dev dependency:
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
import { defineConfig } from "vite"
import eslintPlugin from "vite-plugin-eslint"
export default defineConfig({
plugins: [
eslintPlugin(),
],
})
That’s it! Now, every error ESLint discovers during the runtime will be reported
in realtime in the browser.
Advanced Inertia
59
Structuring apps
While building a frontend, I’m sticking to slightly different file structure
conventions, which help you decouple global app logic from the design and
build a consistent and organized architecture. This approach was first
introduced by Enzo Innocenzi with his Laravel Vite package, and I can’t
recommend it enough.
Here’s the high-level overview of a folder structure I use to group frontendrelated assets:
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
Switching the structure
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 .
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
resolve: {
alias: {
"@": "/resources",
},
},
plugins: [
laravel({
input: "resources/scripts/app.ts",
input: "resources/js/app.ts",
}),
vue(),
],
});
62
Advanced Inertia
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",
],
}
So, let’s take a look at what happens in each directory.
Advanced Inertia
63
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
On the high-level overview, the directory structure looks like this:
resources/views/
├──
components
├──
layouts
├──
pages
└──
app.blade.php
Let’s take a closer look at each type of view component in detail.
Components
When it comes to larger-than-average projects, you want everything to be as
intuitive as possible. Countless style guides recommend storing all partials
within a single directory with special naming rules. I stand against that practice,
as it bloats the directory and makes it hard to name nested components with
time while providing little value.
Unlike the default conventions, we only store global context-unrelated and
headless components within the root components directory.
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
Here’s an example of how a layout structure may look like:
resources/views/layouts/
├──
main
│
├──
partials
│
│
├──
heading.vue
│
│
├──
footer.vue
│
│
└──
sidebar.vue
│
└──
layout-main.vue
├──
auth
└──
landing
We don’t place partial components next to the main component or complex
nested partials next to each other. Storing partial compound components in
separate directories helps us consistently organize files and gives us room to
extend them without bloating the main directory with unrelated components.
Here is a short list of rules I’m trying to follow while building layouts:
Each layout should be stored in a separate directory
Partials with different logic should be divided into even smaller partials so
that every partial component only serves a single purpose
Each kind of complex partial should be placed in a separate directory
Note that we don’t call layout files just “layout,” even though they are placed in
different directories. Prefixing a component with its kind helps your IDE autoimporting dependencies as you start typing <LayoutMain> .
68
Advanced Inertia
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
│
└──
├──
index
│
├──
partials
│
│
├──
filters.vue
│
│
└──
user-card.vue
│
└──
page.vue
└──
show
modal.vue
├──
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
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.
<img src="@/images/logo.png" />
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:
<style src="~/flatpickr.css" />
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.
71
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
Below is a small list of recommendations that help me create intuitive workflows
to the extent that you know in advance how things are named without digging
deep down the file tree.
Advanced Inertia
73
Don’t cut words
Like in the example before, the temptation to shorten Button to Btn might
be strong, but it doesn’t make any sense. We don’t save bytes, but the goal is to
have component names that can be predicted. So, don’t save on characters, as
it doesn’t add any value but could worsen the developer experience.
<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
In natural English, we usually put adjectives and other descriptors before
nouns. However, we have to use less natural language in order to make these
components’ names more predictable and easy to locate.
Name casing
Filenames of single-file components should either be always PascalCase or
always kebab-case. It’s up to you which one to pick, but my personal
preference is kebab-case for filenames and PascalCase in templates. Thanks to
modern IDEs, they can resolve correct imports automatically.
<script setup>
import ButtonOutline from "@/views/components/button-outline.vue"
</script>
<template>
<ButtonOutline />
</template>
Advanced Inertia
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>
75
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: '' }
const items = reactive([])
const addItem = () => items.push(placeholder)
const removeItem = (index) => items.splice(index, 1)
watch(items, (value) => emit('update:value', value))
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"
const props = defineProps({ title: String })
const emit = defineEmits(["update:value"])
const placeholder = { title: '' }
const items = reactive([])
const addItem = (item) => items.push(item)
const removeItem = (index) => items.splice(index, 1)
watch(items => (value) => emit('update:value', value))
</script>
80
Advanced Inertia
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
const amount = ref(0)
const setAmount = (value) => amount.value = value
const show = ref(false)
const toggle = () => show.value = !show.value
</script>
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
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";
title = "That's a new title";
</script>
<template>
<h1>{{ title }}</h1>
</template>
83
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"
const title = ref("Advanced Inertia")
title.value = "This time it updates"
</script>
<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
Computed properties
The Composition provides a similar to the Options API method to create
computed properties. The computed method takes a getter function and
returns a reactive read-only object. So, when the reactive value within the
getter method changes, computed triggers a callback and produces a new
value.
<script setup>
const amount = ref(2);
const doubled = computed(() => amount.value * 2)
amount.value = 4;
</script>
<template>
<!-- outputs 8 -->
<div>{{ doubled }}</div>
</template>
85
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"
const firstName = ref('Taylor')
const lastName = ref('Otwell')
const fullName = computed({
get: () => firstName.value + ' ' + lastName.value,
set: (value) => {
[firstName.value, lastName.value] = value.split(' ')
}
})
</script>
<template>
<input v-model="internalValue" />
</template>
Advanced Inertia
87
Watching changes
Composition API provides two methods to track changes in reactive data
sources — watch and watchEffect . Both methods are similar, except that
the watchEffect() runs a callback function immediately once initialized and
then keeps watching for changes afterward.
const value = ref()
// will be executed on changes only
watch(value, () => {
//
})
// executed immediately on init
watchEffect(value, () => {})
With watch() , you can both track a single reactive or computed property
directly or wrap logic into a callable to watch conditional changes.
const count = ref(0)
watch(() => count.value * 10, () => {
//
})
88
Advanced Inertia
Props
To declare props, Composition API provides the compiler macro
defineProps() , which works similarly to what we used to see in the Options
API.
<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"
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits(["update:modelValue"])
const internalValue = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value)
})
</script>
<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
We connect the backend and frontend together to create a seamless workflow
with the help of automatically generated types, so it’s crucial to have
TypeScript on board.
Since Composition API utilizes mostly plain variables and functions, it can
natively support types without limitations and any tricky moves required.
Typing variables
You can type reactive data sources using basic generic syntax, staying
confident that it only accepts and returns values of desired types.
const count = ref<int>()
const editing = computed<boolean>(() => !!props.model.id)
Advanced Inertia
91
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.
const props = defineProps({
title: String
page: Number
})
// basically the same as
const props = defineProps<{
title: string
page: number
}>()
// you can also use any types besides primitives
const props = defineProps<{
user: UserData
}>
// autocompletion and suggestions from your IDE
props.user.email
92
Advanced Inertia
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
The HOT take
In conclusion, Composition API provides a more flexible way to structure and
build components than Options API. It gives a functional-like (but not actually
“functional”) programming paradigm, utilizing plain JavaScript variables and
functions. Keep in mind that components should be DRY, and too much
flexibility will never be an issue.
So, is there a “right” approach? Developers prefer to discuss this topic
delicately in blog posts, yet here goes the hot take from me: there’s only one
legit way to build Vue apps today — Composition API.
Advanced Inertia
95
Concepts
In this section, we are going to review several common concepts to organize
code in a flexible way.
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 .
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: "pusher",
key: "..."
})
We can access this object anywhere as long as the module containing this
definition is imported somewhere in the app.
Echo.private("chat")
.listen(/* ... */)
Unfortunately, there’s no guarantee the Echo instance is accessible, and the
IDE also can’t help you with a hint unless you define global types manually.
So, here go singletons. Singleton is a pattern that lets you create object
instances that are only resolved once on the first call.
Thanks to modern JavaScript, ES Modules are singletons by default. Singleton
instances are created on the first access, so whenever you import a module,
you are guaranteed to get the same instance from anywhere you import it.
Advanced Inertia
97
Everything you need to make some class a singleton is to export its initializer.
import Echo from "laravel-echo"
export default new Echo({
broadcaster: "pusher",
key: "..."
})
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.
import echo from "@/utils/echo"
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.
npm install -D @types/laravel-echo
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
In the context of Vue applications, a ”composable” is a function that
leverages Vue's Composition API to encapsulate and reuse stateful logic.
Composition API enables us to write reactive code anywhere, introducing the
concept of composables. Composables are functions that implement reactive
and stateful logic, meaning that we can extract common reusable parts of code
into smaller chunks and share them with multiple components.
Don’t confuse composables with stateless utilities. While stateless functions
only take input data and produce the output result once, composables should
handle reactive logic that relies on a state which may change over time.
The most obvious example of a composable is a global state manager. While
there are known robust solutions such as Vuex and Pinia, Composition API
allows building state managers in a natural and more straightforward way.
According to the Vue.js style guide, composables’ names should start with
use .
export function useMode = () => {
const isDark = ref(false)
return { isDark }
}
Advanced Inertia
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"
const mode = useMode()
mode.isDark = true
</script>
Aside from just carrying reactive variables, composables may implement any
state-related methods.
export function useMode = () => {
const isDark = ref(false)
toggle = () => isDark.value = !isDark.value
const setDark = () => isDark.value = false
return { isDark, toggle, setDark }
}
So, composables are a great way to decompose bulky components, extract
parts of reusable logic replacing mixins, and build global state managers.
99
100
Advanced Inertia
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.
import moment from "moment";
export function format(
date: string,
format: string = "MM/DD/YYYY"
): string {
return moment(date).format(format);
}
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
Plugins are self-contained code that usually add app-level functionality to
Vue.
Plugins are something that’s widely used for package development for such
common scenarios as:
Global components
Global directives
Global properties or methods
However, it is still a great concept for any global code that works in the
background and is only executed indirectly.
Plugins can inject global variables and methods into a Vue instance, but I don’t
recommend using any “magic” global syntax that your IDE can’t understand.
Instead, only extract reusable parts of code to composables or utils.
A great example of a local plugin is a notification handler (or response
interceptor), which tracks flash messages in page props, and triggers toast
alerts on successful operations or errors.
Advanced Inertia
103
Let’s assume we set flash messages via the default Laravel session helper
method.
public function update(Request $request)
{
$podcast->update($request->validated());
return back()->with('success', 'Changes saved');
}
Using this approach, we also need to share the flash message on each request
using the Inertia middleware.
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
return array_merge(parent::share($request), [
'notification' => $request->session()->get('success'),
]);
}
}
A plugin could be either a simple function or an object that exposes an
install method that receives the app instance along with additional options
needed.
104
Advanced Inertia
For simple plugins, I stick to the shorter approach, which lets you write a bit
less boilerplate code.
import { App } from "vue"
export default (app: App, payload: any) => {
// the `payload` could be any set of options
}
In this example, we are using the vue-toastification library to display
toast notifications. First, install the package:
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.
import { usePage, router } from "@inertiajs/vue3"
import { useToast } from "vue-toastification"
import "vue-toastification/dist/index.css"
const toast = useToast()
export const notifications = () => {
router.on('finish', () => {
const notification = usePage().props.notification;
if (notification) {
toast(notification)
}
})
}
106
Advanced Inertia
Now, you need to register the plugin to make it globally available.
import { notifications } from "./plugins/notifications"
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:
import Toast from "vue-toastification"
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
Working with data
Inertia.js acts as a layer between a Laravel backend and a JS-powered
frontend. Despite being positioned as just an adapter or “router,” Inertia is a
complete modern approach to building monolith applications.
However, with the default setup, you still have to work with detached
codebases. It doesn’t matter how well-crafted your backend is; once data
touches a frontend, you start dealing with arbitrary datasets.
The principal things we should know while working on a frontend:
What data should we expect from a backend
What params does the backend accept
Advanced Inertia
109
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>
So, what do I think is “wrong” about this piece of code?
On the backend, you explicitly define what data you pass to the frontend.
Whether it’s an Eloquent Resource, a custom data presenter, or even a pure
JSON-encoded model, you have a general idea about what kind of data you’re
going to work with.
110
Advanced Inertia
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:
composer require spatie/laravel-data
112
Advanced Inertia
TypeScript Transformer
Laravel Data has first-party support of another great package by Spatie —
TypeScript Transformer. Having these two in charge of your frontend-related
data means you can reuse data objects defined on a backend, in both the
backend and frontend.
First, install the package and publish the configuration file:
composer require spatie/laravel-typescript-transformer
php artisan vendor:publish --tag=typescript-transformer-config
Next, edit config/typescript-transformer.php and add the following
classes to the collectors and transformers sets:
return [
'collectors' => [
DefaultCollector::class,
DataTypeScriptCollector::class,
],
'transformers' => [
SpatieStateTransformer::class,
SpatieEnumTransformer::class,
DtoTransformer::class,
EnumTransformer::class,
],
];
Advanced Inertia
113
This registers native enum types and laravel-data objects, letting
TypeScript Transformer discover such classes and transform data objects into
TypeScript. However, if you want it to handle plain classes as well (including
enums), you may need to annotate these with the #[TypeScript] attribute.
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
enum Employment: string
{
case OnSite = 'on-site';
case Remote = 'remote';
case Hybrid = 'hybrid';
}
Make sure TypeScript Transformer stores generated definitions in the correct
file. As we made changes to the default structure, you may want to update the
output path in the config file:
return [
'output_file' => resource_path('scripts/types/generated.d.ts'),
];
TypeScript engine can automatically discover global type declarations, making
them available in any file without importing.
114
Advanced Inertia
Resources
Eloquent Resources are used as a transformation layer to define the shape of
data we send to a frontend, and protect sensitive data. While it’s a great
concept in granular control over the JSON representation of models, we don’t
benefit much from it in terms of the frontend-development experience.
Laravel Data objects can replace default Laravel Resources, letting you
explicitly define view-models that can be converted to TypeScript interfaces
after.
Run the command to transform Data objects to TypeScript types:
php artisan typescript:transform
For example, the following data object:
class UserData extends Data
{
public function __construct(
public int $id,
public string $name,
public string $email
) { }
}
Advanced Inertia
115
Will be transformed into this TypeScript type:
type UserData = {
id: number;
name: string;
email: string;
}
When used as a response, laravel-data objects are automatically serialized
into JSON objects.
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"
}
And that’s what a basic form request might look like:
class CreatePodcastRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'url' => ['required', 'string', 'max:255'],
'last_updated_at' => ['date_format:Y-m-d H:i:s'],
'fetch' => ['nullable', 'boolean'],
];
}
}
118
Advanced Inertia
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:
public function store(CreatePodcastRequest $request)
{
$podcast = $request->user()->podcasts()
->create($request->validated());
}
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:
public function store(CreatePodcastRequest $request)
{
$podcast = Auth::user()->podcasts()
->create($request->safe()->except('fetch'));
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.
class PodcastRequest extends Data
{
public function __construct(
// required string
public string $name,
// nullable date-time
public ?DateTime $last_updated_at,
// optional boolean
public Optional|boolean $fetch
) { }
}
120
Advanced Inertia
Data objects let you define validation rules using the attribute notation.
use Spatie\LaravelData\Attributes\Validation;
class PodcastRequest extends Data
{
public function __construct(
#[Validation\Max(z255)]
public string $name,
#[Validation\Url]
public string $feed
) { }
}
Alternatively, you can define rules similarly to basic form requests in a
dedicated method rules . This could be handier when you need to use a
custom rule or a rule with conditional arguments, which is impossible with
attributes.
Advanced Inertia
121
class ProfileData extends Data
{
public function __construct(
public string $name,
public string $email,
) {
}
public static function rules(): array
{
return [
'email' => [
new Validation\Unique(
table: User::class,
ignore: auth()->id()
),
new Validation\Email,
// or
'email',
],
];
}
}
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.
public function store(CreatePodcastRequest $request)
{
$podcast = Auth::user()->podcasts()
->create($request->exclude('fetch'));
// the IDE automatically suggests you a property
if ($request->fetch) {
FetchPodcastEpisodes::dispatch($podcast);
}
}
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:
export type PodcastData = {
name: string
feed: string
fetch: boolean
}
Advanced Inertia
123
A generated TypeScript definition reflects a JSON representation of the
request. And that’s how you can type data carriers using Inertia’s form helper.
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
Automating the generation
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 vitewatch-run . This package can run any commands on specific files’ changes.
Start by installing the plugin:
npm i -D vite-plugin-watch
Then, modify your vite.config.ts configuration file and set up the watcher:
import { defineConfig } from "vite"
import watch from "vite-watch-run"
export default defineConfig({
watch({
pattern: "app/{Data,Enums}/**/*.php",
command: "php artisan typescript:transform",
})
})
Advanced Inertia
This way, when Vite detects changes in tracked files, it will automatically run
the specified command, keeping your TypeScript interfaces up-to-date.
125
126
Advanced Inertia
Wrapping it up
By utilizing laravel-data rich objects for both incoming payload (requests)
and data passed as frontend props (resources), we can automatically get a
TypeScript representation of these objects for free without writing any
TypeScript types manually.
These auto-generated types provide us with a perfect IDE autocompletion for
both frontend and backend, type safety, and help prevent possible runtime
bugs. With this setup, every change in the data shape on a backend also
reflects on your frontend so that you can always be sure that you pass the
correct data back and forth.
The only TypeScript-related thing you will need to care about while building
frontend is to remember what types to apply to your forms and props.
Advanced Inertia
127
Pagination
Dealing with paginated datasets is no more complicated than with plain data.
Laravel Data can handle paginated collections without any extra effort required.
public function index()
{
$users = User::paginate();
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
While laravel-data already wraps data in a familiar pagination format, you
may still need some extra functionality to deal with the state of links and
customize the look of certain buttons (previous page, first page).
I’ve built a simple headless wrapper package around Laravel Pagination. It
supports all basic pagination, Laravel Resources, and Laravel Data.
First, install the package:
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.
import { usePaginator, Paginator } from "momentum-paginator";
const props = defineProps<{
users: Paginator<App.Data.UserData>
}>()
const paginator = usePaginator(props.users);
Advanced Inertia
129
is a generic type provided by momentum-paginator , which
helps your IDE better understand paginated objects you receive from the
backend.
Paginator
The composable could be decomposed into multiple variables, providing you
with the necessary metadata and the list of page objects.
const { from, to, total, pages } = usePaginator(props.users)
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
<script setup lang="ts">
import { usePaginator, Paginator } from "momentum-paginator"
const props = defineProps<{ data: Paginator<any> }>()
const { from, to, total, pages } = usePaginator(props.users)
</script>
<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
In Laravel apps, you can handle authorizations in several ways, including
manually defined gates and policies or with the help of third-party packages.
The framework gives you multiple options to check whether the current user is
allowed to perform some certain action:
Model can(...) method
Route middleware or can(...) method
Direct check method — Gate::check(...)
Controller helper method — authorize(...)
Blade directive — @can
While we still perform these checks on the server side, it’s critical for the
frontend to reflect the state of these permissions as well, as you don’t want a
user to see buttons and links he is not intended to use. Unfortunately, both
Inertia.js and Laravel lack this feature by default.
Thanks to the flexibility of laravel-data resources, we can easily extend its
functionality and mix the results of these checks to the response.
132
Advanced Inertia
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);
}
public function create(User $user)
{
return $user->isAdmin();
}
}
Once the policy class has been created, it needs to be registered.
Registering policies is how we can inform Laravel which policy to use when
authorizing actions against a given model type.
Advanced Inertia
133
In order to register policies, specify them mapped to the corresponding
Eloquent model classes in the policies array of AuthServiceProvider .
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Podcast::class => PodcastPolicy::class,
];
}
Now, we need authorization checks to be performed on returning data to a
frontend.
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:
composer require based/momentum-lock
134
Advanced Inertia
Extend your data classes from DataResource instead of Data provided by
Laravel Data.
use Momentum\Lock\Data\DataResource;
class PodcastData extends DataResource
{
public function __construct(
public int $id,
public string $name
) { }
}
You can either specify the list of abilities manually or let the package resolve
them from the corresponding policy class.
class PodcastData extends DataResource
{
protected $permissions = ['update', 'delete'];
// or
protected string $modelClass = Podcast::class;
}
Advanced Inertia
135
Momentum Lock doesn’t require any additional steps, and you can pass data as
you already do without any extra changes required.
public function index()
{
return inertia('podcasts/index', [
'podcasts' => PodcastData::collection(Podcast::paginate()),
]);
}
Whenever you return a DataResource instance as a response or Inertia prop,
it will automatically perform authorization checks and mix these results to the
JSON object.
{
"id": 16,
"name": "Full Stack Radio",
"permissions": {
"viewAny": true,
"view": false,
"create": true,
}
}
136
Advanced Inertia
TypeScript definitions
As we rely heavily on TypeScript in this guide, we can also benefit from
extensive autocompletion and type safety on the frontend.
Register DataResourceCollector in the TypeScript Transformer
configuration file — typescript-transformer.php . This class helps
TypeScript Transformer handle DataResource classes and append the list of
available permissions to generated 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
Down the frontend
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.
<script lang="ts" setup>
import { can } from "momentum-lock"
const props = defineProps<{ podcasts: PodcastData[] }>()
</script>
<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
Unlike normal single-page applications, with Inertia, we don’t define routes on
the frontend. Instead, all routing is defined server-side in routes/*.php as
you would usually do with basic Laravel applications.
The most popular tool to handle named Laravel routes on the frontend is Ziggy.
It’s a great library that only comes with a single downside — it doesn’t have
built-in TypeScript support; thus, it doesn’t know about available routes and
their bindings until you call the route method.
As we focus on building a robust and predictable workflow, it’s essential to keep
your routes in sync with the backend.
Advanced Inertia
139
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 .
composer require based/momentum-trail
Publish the config file with the following command:
php artisan vendor:publish --tag=trail-config
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
The frontend package is framework-agnostic and works well with any
framework. Install the package.
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 ).
import { defineRoutes } from "momentum-trail"
import routes from "./routes.json"
defineRoutes(routes);
Alternatively, you can register routes using a Vue 3 plugin coming as a part of
the package.
import { trail } from "momentum-trail"
import routes from "./routes.json"
createInertiaApp({
setup({ el, app, props, plugin }) {
createApp({ render: () => h(app, props) })
.use(trail, { routes })
}
})
Advanced Inertia
Import the helper in your .vue or .ts files and enjoy autocompletion and
type-safety for both route and current methods.
<script lang="ts" setup>
import { route, current } from "momentum-trail"
const props = defineProps<{
user: App.Data.UserData
}>()
const submit = () => form.put(
route("users.update", props.user)
)
</script>
<template>
<a
:class="[
current('users.*') ? 'text-black' : 'text-gray-600'
]"
:href="route('users.index')">
Users
</a>
</template>
141
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.
import { defineConfig } from "vite"
import { watch } from "vite-plugin-watch"
export default defineConfig({
watch({
pattern: "routes/*.php",
command: "php artisan trail:generate",
}),
})
144
Advanced Inertia
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.
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
return array_merge(parent::share($request), [
'auth' => fn () => $request->user()->only('id', 'name'),
]);
}
}
Advanced Inertia
145
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.
class SharedData extends Data
{
public function __construct(
public ?UserData $user = null,
) { }
}
You can create the instance of SharedData within the
HandleInertiaRequests middleware as you usually do and merge its
contents with the default Inertia response.
public function share(Request $request)
{
$state = new SharedData(
user: Auth::check() ? UserData::from(Auth::user()) : null,
);
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;
class SharedData extends Data
{
public function __construct(
#[TypeScriptType(UserData::class)]
public Closure|null $user = null,
) { }
}
Advanced Inertia
When Inertia performs a partial visit, it will determine which data is required,
and only then will it evaluate the closure.
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
$state = new SharedData(
user: fn() => UserData::from(Auth::user()),
);
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.
usePage<{ user: App.Data.UserData }>().props
147
148
Advanced Inertia
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"
declare module "@inertiajs/vue3" {
export declare function usePage(): Page<App.Data.SharedData>
}
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.
return to_route('users.index')->with('success', 'User deleted');
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.
class NotificationData extends Data
{
public function __construct(
public NotificationType $type,
public string $body
) { }
}
150
Advanced Inertia
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.
const page = usePage()
// string
page.props.notification .body
// success | error
page.props.notification .type
Advanced Inertia
151
Extend the SharedData class to append the contents of a notification to the
response. The shareNotification method checks if notification is present
on the session and initializes the NotificationData property.
class SharedData extends Data
{
public function __construct(
public ?NotificationData $notification = null
) {
$this->shareNotification();
}
protected function shareNotification(): void
{
if (session('notification')) {
$this->notification = new NotificationData(
...session('notification')
);
}
}
}
The best way to trigger notifications on redirect is to extend
RedirectResponse class with a custom method instead of relying on
with(...) and arbitrary values.
152
Advanced Inertia
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:
php artisan make:provider InertiaServiceProvider
This command will generate a basic service provider class in the
app\Providers directory. Register the provider in the config\app.php file.
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.
class InertiaServiceProvider extends ServiceProvider
{
public function register()
{
RedirectResponse::macro(
'flash',
function (NotificationType $type, string $body) {
session()->flash('notification', [
'type' => $type,
'body' => $body,
]);
/** @var RedirectResponse $this */
return $this;
});
}
}
Starting this point, you can trigger flash notifications using the flash method.
return back()->flash(NotificationType::Success, 'Info updated');
154
Advanced Inertia
You can also create dedicated methods for the most common notification
types.
RedirectResponse::macro('success', function (string $body) {
return $this->flash(NotificationType::Success, $body);
});
RedirectResponse::macro('error', function (string $body) {
return $this->flash(NotificationType::Error, $body);
});
Here’s an example of usage in a controller:
public function delete(User $user)
{
$user->delete();
return back()->success('The user has been deleted');
}
Advanced Inertia
155
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 idehelpers.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
Handling on the frontend
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.
import { usePage, router } from "@inertiajs/vue3"
import { useToast } from "vue-toastification"
import "vue-toastification/dist/index.css"
const toast = useToast()
export const notifications = () => {
router.on("finish", () => {
const notification = usePage().props.notification
if (notification) {
toast(notification)
toast(notification.body, { type: notification.type })
}
})
}
Please refer to the Plugins section for a complete explanation.
Advanced Inertia
157
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 package unplugin-auto-import helps Vite automatically resolve commonly
used imports on-demand.
Basically, it lets you write code that only reflects the actual component’s logic
with no overused imports with rare exceptions so that your components can
stay as clean as possible.
For Laravel developers, the best example is the helper methods. We are using
such helpers as auth() , session() , and redirect() everywhere without
even caring what the end classes responsible for these actions are.
Thanks to global type definitions, TypeScript allows us to bring the same kind
of feature to the frontend development with the help of unplugin-autoimport , keeping the IDE support.
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.
import { defineConfig } from "vite"
import autoimport from "unplugin-auto-import/vite"
export default defineConfig({
plugins: [
autoimport({
vueTemplate: true,
dts: "resources/scripts/types/auto-imports.d.ts",
imports: ["vue"],
})
],
})
In the configuration above, we instruct the plugin to automatically handle Vue.js
imports.
Starting this point, you can use any Vue methods without importing them
manually.
import { computed, ref, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
onMounted(() => console.log('mounted'))
160
Advanced Inertia
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.
export default defineConfig({
plugins: [
autoimport({
vueTemplate: true,
dts: "resources/scripts/types/auto-imports.d.ts",
imports: [
"vue",
{ "momentum-lock": ["can"] },
{ "momentum-trail": ["route", "current"] },
{ "@inertiajs/vue3": ["router", "useForm", "usePage"] },
],
})
],
})
Advanced Inertia
161
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.
<script lang="ts" setup>
import { useForm, router } from "@inertiajs/vue3"
import { route } from "momentum-trail"
import { can } from "momentum-lock"
const props = defineProps<{ company: App.Data.CompanyData }>
const form = useForm(props.company)
const submit = () => form.post(
route("companies.update", props.company)
)
const destroy = () => router.delete(
route("companies.destroy", props.company)
)
</script>
<template>
<button @click="destroy" v-if="can(company, 'delete')">
Delete
</button>
</template>
162
Advanced Inertia
Global components
Typically, global components are considered an anti-pattern. They have a major
drawback — your IDE just can’t understand them.
The package unplugin-vue-components solves this limitation and lets you use
components in templates as you would usually do without registering and
importing them manually.
Like unplugin-auto-import , the unplugin will import components on
demand and bring you perfect IDE support with automatically generated global
typings.
If you register the parent component asynchronously (or lazy route), the
auto-imported components will be code-split along with their parent.
Install the package.
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.
import { defineConfig } from "vite"
import components from "unplugin-vue-components/vite"
export default defineConfig({
plugins: [
components({
dirs: ["resources/views/components"],
dts: "resources/scripts/types/components.d.ts",
}),
],
})
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.
<script lang="ts" setup>
import InputText from "@/views/components/BaseButton.vue"
const name = ref('')
</script>
<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.
import { defineConfig } from "vite"
import components from "unplugin-vue-components/vite"
export default defineConfig({
plugins: [
components({
dirs: ["resources/views/components"],
dts: "resources/scripts/types/components.d.ts",
resolvers: [
(name: string) => {
const components = ["Link", "Head"]
if (components.includes(name)) {
return { name, from: "@inertiajs/vue3" }
}
},
],
}),
],
})
Advanced Inertia
165
Conclusion
While auto-imports is controversial, I still find it a great opportunity to simplify
your source files, cleaning up the unnecessary clutter and letting you only care
about actual components’ logic.
Here’s the before-after example from the demo project Mixjobs coming with
the Premium package of the book.
<script lang="ts" setup>
import { useForm, Head } from "@inertiajs/vue3"
import { route } from "momentum-trail"
import LayoutGuest from "@/views/layouts/guest/layout-guest.vue"
import InputLabel from "@/views/components/input-label.vue"
import InputText from "@/views/components/input-text.vue"
import InputError from "@/views/components/input-error.vue"
import ButtonPrimary from "@/views/components/button-primary.vue"
const form = useForm<App.Data.CompanyData>({
name: "Advanced Inertia Inc.",
url: "https://advanced-inertia.com",
logo: null,
about: "",
})
const submit = () => form.post(route("register.company"))
</script>
166
Advanced Inertia
<template>
<LayoutGuest>
<Head title="Set up your company" />
<form class="space-y-4" @submit.prevent="submit">
<div>
<InputLabel for="name" value="Name" />
<InputText
id="name"
v-model="form.name"
type="text"
class="mt-1 block"
required
autofocus
autocomplete="name" />
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div class="flex flex-col gap-4">
<ButtonPrimary :disabled="form.processing">
Submit
</ButtonPrimary>
</div>
</form>
</LayoutGuest>
</template>
Advanced Inertia
167
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
Download
Study collections