Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

Frontend

Inertia

Build single-page React, Vue or Svelte apps with server-side routing and controllers through Vukem's Inertia v3 adapter.

Última atualização:

Vukem ships its own Inertia v3 adapter in the inertia package, with starters for React, Vue and Svelte.

Terminal
vukem new app --inertia react         # or vue, svelte
vukem install:inertia --force --react # turn an HTMX starter into an Inertia app
vukem serve                           # Go app with reload + npm run dev

Rendering pages#

Go
func (p *PostController) Index(c *web.Ctx) error {
	posts, err := p.posts.All(c.Context())
	if err != nil {
		return err
	}

	return inertia.Render(c, "Posts/Index", inertia.Props{
		"posts":   posts,
		"filters": inertia.Optional(func(ctx context.Context) (any, error) { return loadFilters(ctx) }),
		"stats":   inertia.Defer(statsFn).Group("sidebar"),
	})
}

The starter wires it for you:

Go
vukem.WithInertia(inertia.Config{})              // bootstrap/app.go → app.Inertia
r.Use(app.Inertia.Middleware, shareProps(app))   // routes/web.go: Vary, version check, 302→303, fragment redirects

inertia.RenderStatus(c, status, component, props) renders a page with another status, such as a 404, 410 or 503 error page: the Inertia client shows any response carrying X-Inertia as a page. Do not call c.Status before Render, which commits the headers. Statuses without a body (1xx, 204, 205, 304), redirects and 409 (reserved by the protocol) return an error.

Go
return inertia.RenderStatus(c, http.StatusGone, "Errors/Gone", inertia.Props{"reason": "expired"})

The root view resources/views/app.html holds the CSP nonce meta, {{vite}}, {{.Head}} in <head> and {{.Inertia}} in <body>. The initial page is a <script data-page="app" type="application/json"> with / escaped.

Props#

PropBehaviour
plain value or func(ctx) (any, error)always sent; functions resolve lazily
Optional(v)only when a partial reload asks for it
Always(v)even when a partial reload excludes it
Defer(v).Group(g).Rescue()loaded after the first render, grouped; Rescue keeps the page alive if it fails
Merge(v) with .Append(paths...) / .Prepend(paths...) / .MatchOn(fields...), DeepMerge(v)merged into the client's current value
Once(v).As(key).Fresh().Until(t)sent once and remembered by the client
Scroll(v, meta), ScrollPage(orm.Page), ScrollCursor(orm.CursorPage, name)infinite scrolling

Modifiers chain (Defer(v).Merge(), Merge(v).Once()). Nested maps resolve recursively, top-level "a.b" keys unpack, and partial reloads filter by dotted path (only, except, reset).

Shared props: inertia.Config.Shared for global ones, inertia.Share(c.Context(), key, value) per request. The starters share the auth user and flash messages.

Translations (i18n)#

inertia.I18n sends the i18n catalog to your components, so Vue and React use the same JSON files as the server:

Go
locale, err := i18n.Middleware(bundle, i18n.MiddlewareConfig{}) // resolves the locale (cookie, ?lang=, Accept-Language)
if err != nil {
	return err
}

r.Use(
	locale,
	in.Middleware,
	inertia.I18n("ui.", "nav."), // only keys under ui.* and nav.* reach the browser
)

Every page then gets three props:

PropExampleNotes
locale"pt-BR"the locale resolved for this request
locales["en", "pt-BR"]for a language switcher
messages{"nav.home": "Início"}flat keys, with fallback to the default locale; plural messages are objects ({"one": "...", "other": "..."})

messages is a once prop keyed by locale: the browser downloads the catalog on the first visit and again only when the locale changes.

A small helper is enough on the client. It fills {name} placeholders and picks plural forms with Intl.PluralRules, like the server's i18n.T:

TypeScript
// resources/js/lib/i18n.ts
type Message = string | Record<string, string>

export function translate(
	messages: Record<string, Message>,
	locale: string,
	key: string,
	params: Record<string, string | number> = {},
): string {
	let msg = messages[key]
	if (msg === undefined) return key
	if (typeof msg !== "string") {
		const n = Number(params.count)
		msg = (n === 0 && msg.zero) || msg[new Intl.PluralRules(locale).select(n)] || msg.other || key
	}
	return msg.replace(/\{(\w+)\}/g, (m, k) => (k in params ? String(params[k]) : m))
}
Text
<!-- Vue -->
<script setup lang="ts">
import { usePage } from "@inertiajs/vue3"
import { translate } from "@/lib/i18n"

const page = usePage()
const t = (key: string, params = {}) => translate(page.props.messages, page.props.locale, key, params)
</script>

<template><a href="/">{{ t("nav.home") }}</a></template>
TSX
// React
import { usePage } from "@inertiajs/react"
import { translate } from "@/lib/i18n"

export function useT() {
	const { messages, locale } = usePage().props as { messages: Record<string, string>; locale: string }
	return (key: string, params = {}) => translate(messages, locale, key, params)
}

To switch language, link to any page with ?lang=pt-BR (or set the locale cookie) and the next visit carries the new catalog.

Requests and redirects#

AreaSupport
Versionhash of the Vite manifest; a GET with an old version gets 409 + X-Inertia-Location
Partial reloadsPartial-Component, Partial-Data, Partial-Except, X-Inertia-Reset, error bags, prefetch (Purpose: prefetch)
Redirects302 → 303 after PUT/PATCH/DELETE; inertia.Location(c, url) forces a full visit (409 for external URLs); inertia.PreserveFragment(c) keeps the URL fragment
Validationerrors flashed and 303 back, surfaced as props.errors
Flashinertia.Flash(c, key, value) fills page.flash once; survives redirects, not consumed by prefetch
HistoryConfig.EncryptHistory (needs a secure context) or inertia.EncryptHistory(ctx, bool) per page; inertia.ClearHistory(c) on logout

Precognition#

Go
r.Post("/users", inertia.Precognitive(schema, h))

Precognition: true requests only validate: 204 with Precognition-Success, or 422 {message, errors} filtered by Precognition-Validate-Only. Vary: Precognition is always set.

Server-side rendering#

Go
inertia.Config{SSR: &inertia.SSRConfig{Bundle: "bootstrap/ssr/ssr.js"}}
  • Pages are POSTed to the Node SSR server (/render, default 127.0.0.1:13714, 3s timeout). Any failure falls back to client-side rendering and is reported to OnFailure (a warning log by default).
  • In development the Vite plugin endpoint ({hot}/__inertia_ssr) is used.
  • Except path prefixes and inertia.DisableSSR(ctx) skip SSR per request.
  • vukem ssr:start runs node bootstrap/ssr/ssr.js and restarts it when it crashes; vukem ssr:check probes /health.

SSR is off by default.

Typed routes#

Terminal
vukem codegen:routes            # resources/js/routes.ts (-o file, -o - for stdout)
TypeScript
import { route, routes, type RouteName } from "./routes";

route("posts.show", { id: 7 });                     // "/posts/7"
route("files.show", { path: "a b/c" }, { page: 2 }); // "/files/a%20b/c?page=2"
routes["posts.store"].method;                       // "POST"

The command boots the app in warmup mode (config and route definitions, no Boot, no connections) and writes only named routes. Params are typed per route; regenerate after changing routes and commit the file. For typed API calls, see client generation.

Testing#

Go
app.Client(t).Inertia("").Get("/posts").
	AssertInertia("Posts/Index").
	HasProp("posts", 3).
	MissingProp("stats").
	PropEquals("filters.q", "go")

Flash data (inertia.Flash) lives in page.flash, not in props. FlashEquals(key, v) / MissingFlash(key) on the page read only page.flash. AssertFlash(key, v) / AssertNoFlash(key) on any response also look in the session, so they work before the redirect is followed:

Go
c := app.Client(t).Inertia("")
c.PostJSON("/posts", form).AssertRedirect("/posts").AssertFlash("status", "Post saved.")
c.Get("/posts").AssertInertia("Posts/Index").FlashEquals("status", "Post saved.")

Works on first visits (HTML) and Inertia visits (JSON). Inertia("") sends the app's current asset version, so tests keep passing after bun run build; pass any other value (Inertia("stale")) to test the 409 version-mismatch reload. Inertia(version) also sends the Inertia Accept header, so PostJSON behaves like a form submission.