Skip to content

Inertia and Laravel

Use app-side helpers for typed Inertia props, URL-backed filters, shared pagination, and awaitable forms.

On this page

The reference implementation of each one lives in the library repository's ai/guides/inertia-helpers.md. This page is the shape of the toolkit: what exists, why, and which library component each part pairs with.

What Inertia changes

In Inertia there is no client data layer. Data arrives as page props and changes through visits. There is no store to read, no query cache to invalidate, and no fetch to await — the server re-renders and the page props change underneath you.

That model is a good fit for this library, whose components already report state rather than owning it — see Fetching data. What it lacks is a typed surface for the props themselves, a place for filters to live, and a way to await a form submit. That is what the toolkit adds, so pages stay thin.

What to install

Copying the toolkit pulls in real npm dependencies beyond your app's own @inertiajs/vue3 and vue. Install them explicitly:

bash
npm i @vueuse/core object-code query-string

@vueuse/core backs the keyed contexts and a couple of the UI utilities; object-code powers the hash comparison behind isEqual; query-string backs the three URL helpers.

Reading page props

useProp and useOptionalProp give the page's props one typed surface, so a component reads useProp('customer') instead of reaching into usePage().props and asserting a type at every call site.

useLazyProp and useLazyPropGetter cover Inertia's lazy props — the ones the server only sends when asked — so a panel that loads its detail on open does not have to hand-roll the request-and-track dance.

Filters that live in the URL

Three helpers, in increasing scope.

useRouteQuery is a single query parameter as a writable ref. Reading tracks the current URL; writing merges the parameter and performs a replace visit, so the server re-renders with the new value.

ts
const status = useRouteQuery('status', 'open');

Writing the current value is a no-op, which is what makes it safe to sync from a server response without a visit loop.

useQuery and useQueryObject widen that to a parsed query string and to a whole object of filters.

Bind these straight into the components. A BbSelect filter and a BbTable's v-model:sort are both just refs, and once they are URL-backed the page is shareable, restorable and back-button-correct without any extra code:

vue
<BbSelect v-model="status" :items="statuses" label="Status" />
<BbTable v-model:sort="sort" :items="rows" :columns="columns" />

Sharing data down a page

Keyed contexts, instead of prop drilling. usePaginatedResponseContext publishes the Laravel paginator shape — data plus a meta block of total, per_page, current_page and last_page — so a toolbar, a table and a pager can each read it where they stand.

useTableSelectionContext and useTableContext do the same for a table's selection and its own state, which is what lets a bulk-action bar outside the table know what is selected inside it.

Forms that return promises

usePromiseForm is Inertia's useForm with promise semantics: post, put, patch and remove resolve on success and reject on validation errors. form is still the underlying Inertia form, so form.errors, form.isDirty and form.reset() all work.

The payoff is that a submit becomes awaitable, which is exactly what the library's own automatic loading states expect:

vue
<BbButton type="submit" @click="() => submit(`/customers/${id}`, 'put')">
    Save
</BbButton>

BbButton shows its spinner and disables itself for the life of that promise with nothing else written, and useConfirm's onClick takes the same shape.

One edge worth knowing: a non-Inertia response — a 423 entity lock, a 419 CSRF mismatch — fires neither onSuccess nor onError, only onFinish. The helper settles the promise there too, so a failed visit releases the busy state instead of hanging the form forever.

Guarding a dirty form

useConfirmOnRouteLeave intercepts Inertia visits and asks through useConfirm before leaving. It needs <BbConfirm /> mounted at the app root, like any other confirm.

Bind disabled to !form.isDirty and the guard only appears when there is something to lose. Server redirects after a whitelisted action carry an X-Whitelisted-Action header, so a saving visit's own redirect is never blocked by the guard it just triggered — the failure mode this exists to prevent.

useHasChanged gives you the same dirty signal for state that is not an Inertia form.

The rest of the toolkit

The remaining helpers are small and independent. Take the ones you need:

HelperFor
useLoadingWrapping any async function with a loading ref
useAsyncFnThe same, with the result and error surfaced
useResourceContextSharing one resource's state across a page
useDeleteCmdThe delete-with-confirm command, wired once
useScreenSizesBreakpoint state for adaptive layouts
useIndexByIdTurning a list into a keyed lookup
useUntilAwaiting a reactive condition

None of them are required. The three that change how a page is written are useRouteQuery, usePromiseForm and the paginated context — start there and add the rest when a page asks for them.

Adopt them in that order. Put one filter in the URL, convert one submit to a promise, then introduce a context only when two separate regions need the same page data. Copying the full toolkit up front adds dependencies and abstractions before the page has a use for them.