© 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