Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Frontend

Inertia

Inertia lets you write your pages as React, Vue, Svelte, Solid or Angular components, while your routes and controllers stay on the server, with no API to build in between.

Introduction#

An Inertia page is a component in resources/js/pages. Your controller returns the page's name and its props; Inertia renders the component with them, in the browser on later visits and, with server-side rendering, on the server for the first one. Links and forms make requests as a single-page app does, and the server answers with the next page's props as JSON.

Oriel follows inertia-laravel on the server and speaks the protocol of the official Inertia clients. Its adapters, one per framework, add typed pages and Oriel's route():

FrameworkPackageStarter kit
React@orieljs/inertia-react--inertia=react
Vue@orieljs/inertia-vue--inertia=vue
Svelte@orieljs/inertia-svelte--inertia=svelte
Solid@orieljs/inertia-solid--inertia=solid
Angular@orieljs/inertia-angular--inertia=angular

The React, Vue and Svelte adapters are the official Inertia adapters, re-exported with Oriel's additions. The Solid and Angular adapters are Oriel's own, with the same API in each framework's idiom. A new app from an Inertia starter kit has everything below set up.

Server-side setup#

Creating responses#

An action returns a page with inertia(), its component's name and its props:

app/Http/Controllers/PostController.ts
import { inertia } from '@orieljs/framework'import { Post } from '../../Models/Post'import { Controller } from './Controller'export class PostController extends Controller {  /** Show the blog's posts. */  async index() {    return inertia('Posts/Index', {      posts: await Post.latest().get(),    })  }}

The component's name is its path under resources/js/pages, without the extension: Posts/Index is resources/js/pages/Posts/Index.tsx (or .vue, .svelte, .component.ts). Models in the props reach the page as their JSON, so their hidden attributes stay on the server.

A page with no data of its own needs no controller: Route.inertia() routes a URI to it.

routes/web.ts
import { Route } from '@orieljs/framework'Route.inertia('/about', 'About').name('about')

The root view#

The first visit loads the root view, resources/views/app.tsx, which writes the HTML document around the page. It places the page's data with inertiaBody(page), the server-rendered head with inertiaHead(page), and your scripts and styles with vite(). A React app adds viteReactRefresh() before vite():

resources/views/app.tsx
import { inertiaBody, inertiaHead, vite, viteReactRefresh } from '@orieljs/framework'import type { InertiaPage } from '@orieljs/framework/contracts'export default function App({ page }: { page: InertiaPage }) {  return (    <html lang="en">      <head>        <meta charset="utf-8" />        <meta name="viewport" content="width=device-width, initial-scale=1" />        {inertiaHead(page)}        {viteReactRefresh()}        {vite(['resources/css/app.css', 'resources/js/app.tsx'])}      </head>      <body>{inertiaBody(page)}</body>    </html>  )}

The middleware#

HandleInertiaRequests, in app/Http/Middleware, extends InertiaMiddleware, and bootstrap/app.ts appends it to the web group. It answers in the Inertia protocol: a JSON page on Inertia requests, a 409 when your assets changed since the page was loaded, and a 303 after a PUT, PATCH or DELETE that redirects. node oriel inertia:middleware writes one for an app that hasn't.

Its share() gives every page some props, merged with the validation errors it shares itself:

app/Http/Middleware/HandleInertiaRequests.ts
import { Auth, config, InertiaMiddleware } from '@orieljs/framework'import type { HttpRequest } from '@orieljs/framework/contracts'import type { User } from '../../Models/User'export class HandleInertiaRequests extends InertiaMiddleware {  /** The root view loaded on the first page visit: resources/views/app.tsx. */  protected override rootViewName = 'app'  /** Props shared with every page. */  override async share(request: HttpRequest) {    const user = await Auth.user<User>()    return {      ...(await super.share(request)),      name: String(config('app.name')),      auth: { user: user === undefined ? null : { id: user.id, name: user.name, email: user.email } },    }  }}

version() returns your assets' version: a hash of app.asset_url when you set one, else of public/build/manifest.json. A page loaded with an older version is reloaded in full on its next visit.

Shared data#

Besides the middleware's share(), Inertia.share() shares props from anywhere in a request, such as a controller:

Inertia.share('appName', 'Blog')

Inertia.flash() stores data for the next page only, which reads it as page.flash, as session flash data works.

Redirects#

Redirect as anywhere else in your app: redirect().route('posts.index'), or back(). Inertia follows the redirect and shows the next page. To leave the app, to an external site or a page that isn't Inertia's, Inertia.location() makes the browser load the URL in full:

Route.get('/billing', () => Inertia.location('https://example.com/billing'))

Props: deferred, optional and merged#

A prop can be a function, which runs only when the prop is sent. These helpers choose when that is:

HelperThe prop is sent
Inertia.defer(fn, group?)In a request the page makes right after it renders, so the page shows sooner; props of one group come together
Inertia.optional(fn)Only in a partial reload that asks for it
Inertia.always(value)In every response, even a partial reload that didn't ask for it
Inertia.merge(value), Inertia.deepMerge(value)And merged into the page's current value on a reload, such as the next page of a list
Inertia.once(fn)Once: the client keeps it for the next pages
Inertia.scroll(paginator)As a page of an infinite scroll, with its page numbers
app/Http/Controllers/DashboardController.ts
import { Inertia, inertia } from '@orieljs/framework'import { Post } from '../../Models/Post'import { Controller } from './Controller'export class DashboardController extends Controller {  async index() {    return inertia('Dashboard', {      latest: await Post.latest().limit(5).get(),      drafts: Inertia.defer(() => Post.whereNull('published_at').get()),      stats: Inertia.optional(() => Post.count()),    })  }}

A partial reload asks the server for some props only, such as router.reload({ only: ['stats'] }) in the page.

Client-side setup#

The browser entry, resources/js/app.tsx (app.ts in the Vue, Svelte and Angular kits), starts Inertia with your pages' folder:

resources/js/app.tsx
import { createInertiaApp } from '@orieljs/inertia-react'const appName = import.meta.env.VITE_APP_NAME || 'Oriel'createInertiaApp({  pages: './pages',  title: (title) => (title ? `${title} – ${appName}` : appName),})

title formats every page's title. The oriel() Vite plugin sets up each framework's own Vite plugin.

Pages#

A page component receives its props. They are typed: node oriel inertia:types reads the inertia() calls of your controllers and routes and writes resources/js/oriel/pages.d.ts, so each page knows its props, and the props your middleware shares. The kits' Vite plugin runs it when Vite starts, and again when your server code changes.

resources/js/pages/Posts/Index.tsx

resources/js/pages/Posts/Index.tsx

import { Head, Link, route, type PageProps } from '@orieljs/inertia-react'export default function Index({ posts }: PageProps<'Posts/Index'>) {  return (    <>      <Head title="Posts" />      <ul>        {posts.map((post) => (          <li key={post.id}>            <Link href={route('posts.show', post.slug)}>{post.title}</Link>          </li>        ))}      </ul>    </>  )}

Link (in Angular, the oLink directive on an <a>) visits a page without reloading the document. route() gives a named route's URL, typed: once node oriel route:types has run, a wrong name or a missing parameter is a type error. isCurrentRoute('posts.*') tells you whether the current page's route matches, for an active link. Both work during server-side rendering, from the URL being rendered.

router makes visits from code: router.visit(url), router.post(url, data), router.reload({ only: ['stats'] }).

Forms#

useForm() holds a form's data, sends it, and gives back the validation errors, one per field, and whether the request is in flight:

resources/js/pages/Posts/Create.tsx

resources/js/pages/Posts/Create.tsx

import { route, useForm } from '@orieljs/inertia-react'import type { FormEvent } from 'react'export default function Create() {  const { data, setData, post, processing, errors } = useForm<{ title: string }>({ title: '' })  const submit = (event: FormEvent) => {    event.preventDefault()    post(route('posts.store'))  }  return (    <form onSubmit={submit}>      <input value={data.title} onChange={(event) => setData('title', event.target.value)} />      {errors.title && <p className="error">{errors.title}</p>}      <button type="submit" disabled={processing}>        Publish      </button>    </form>  )}

A failed validation sends the user back to the page, with the errors (see Displaying errors in Inertia pages). Each adapter also has a Form component (Angular's form[oForm]), which sends a plain HTML form's fields without useForm().

Precognition#

On a route with the precognitive middleware, the React, Vue and Svelte forms can validate a field while the user fills it in: give useForm() the method and the URL first, useForm('post', route('posts.store'), { title: '' }), then call form.validate('title'); Form has a validate() too. The Solid and Angular forms don't offer Precognition yet. Precognition describes the server side, and why the action validates first.

More Inertia features#

Each adapter has Inertia's components and hooks for the props above, under Inertia's names (Angular's are directives):

FeatureReact, Vue, Svelte, SolidAngular
Deferred props, with a fallback while they loadDeferredoDeferred directive
Loading props when an element scrolls into viewWhenVisibleoWhenVisible directive
Infinite scrolling of an Inertia.scroll() propInfiniteScroll<o-infinite-scroll>
Reloading props on an intervalusePoll()usePoll()
Prefetching pages before a clickusePrefetch(), prefetch on LinkusePrefetch(), prefetch on oLink
Keeping local state in the browser's historyuseRemember()useRemember()
The page's title and meta tagsHead (Svelte: <svelte:head>)<o-head>

They work as Inertia's documentation describes, at inertiajs.com.

Server-side rendering#

The React, Vue, Svelte and Solid kits render the first visit on the server, so it arrives as HTML. npm run build builds the server bundle from resources/js/ssr.* into bootstrap/ssr, and node oriel inertia:start-ssr runs it; when no SSR server answers, the pages are rendered in the browser. Inertia SSR and The SSR server cover running it. inertia.ssr.enabled, from INERTIA_SSR_ENABLED, turns it off.

History encryption#

Inertia keeps each page's props in the browser's history, so the back button can show them again. To keep a signed-in user's data out of it after they sign out, encrypt it: INERTIA_ENCRYPT_HISTORY=true encrypts every page's state, and the inertia.encrypt middleware does it for some routes. Inertia.clearHistory() clears the encrypted history, such as when a user signs out.

Adapter differences#

React, Vue, SvelteSolidAngular
Server-side renderingYesYesNo
PrecognitionYesNoNo
useHttp()YesNoNo

The Angular adapter's other limits are in its package's README: track a form's array rows by $index or a stable id, not by the item, since useForm copies the objects along a dotted path on each write; and once a page uses <o-head>, a later Title.setTitle() is overwritten.

Privacy policy ·

esc