A Custom Inertia.js Client

This is a passion project I began a year ago while helping maintain the Inertia.js Svelte adapter. While working with the Inertia core, I found it to be unnecessarily overengineered and full of pain points, especially when trying to customize anything. So, here's the result of my recent efforts!

TL;DR

The package is not yet finished or documented, so I don't recommend using it in production unless you really know what you're doing. You're still welcome to try it out, submit pull requests, and help me write the docs (I hate writing docs, T_T).

Background

It all started about a year ago when I was working on an AI bookwriting project using Laravel + Inertia + Svelte. At the time, there were no type annotations, which made things a bit painful. So I decided to implement TypeScript support, as previous PR was staled for like a year or something...

It took 3 months for my PR to be reviewed! And it was just an adapter — not even the core library!

Anyway, while working on a client project in parallel, I ended up spending a few months maintaining a fork of the Inertia Svelte adapter. And it felt like HELL: quirky layout behavior that most projects don't even need, a ginormous form helper, and a router that’s just an unnecessarily complex wrapper over Axios. Honestly, I’d often prefer using Axios directly instead of Inertia.

And if you happened to have Axios already in your dependencies, Inertia sometimes broke it because it modified its default config. That pushed me to write a custom client—something easier to use in future projects. I almost gave up when they released version 2.0, but I recently came back to it after leaving my job and feeling pretty down. I just wanted to build something I enjoy. And… I actually really like what I’ve been cooking.

Goals

When building this project, I aimed to:

  • Ditch most of the sugary syntax from Inertia’s router.
    I wanted a router with a syntax identical to Axios, so I wouldn’t have to keep checking Inertia’s docs. Just give me simple async/await and try/catch.

  • Make it truly framework-agnostic.
    Let’s be honest—if a client requires an adapter for every frontend framework, it’s not really framework-agnostic. I’m okay with backend adapters (different frameworks handle responses differently), but the frontend? It’s all just JavaScript. I wanted to connect it to any frontend framework in just a few lines of code.

  • Own all the frontend logic in my projects.
    Trying to customize layout behavior or add transitions is impossible when everything is buried deep in Inertia’s adapters. I want a simple SPA that reactively updates based on backend props.

So How Did I Build It?

My initial idea was to dispatch custom events on page changes and update the reactive page state based on the framework. But then I realized I was just wrapping Axios in a custom router again.

So I thought: "What if I make the page state reactive inside the library itself?"
And that was my best idea yet.

I implemented a simple signal and effect system, added a wrapper around effect so you can subscribe to page state in a single line—and that was it. Binding it to any framework was suddenly trivial (I'll show this later).

I still needed a router though. Instead of wrapping Axios, I used Axios directly—but created a separate instance, so it wouldn't interfere with others. As I was writing interceptors for Inertia headers, I thought: "What if I want to customize the backend too?" So I built an extension API rather than hardcoding anything.

The last challenge was the useForm helper. I thought it would be impossible to build reactively without framework adapters—but with some Proxy magic, I created a form object that’s reactive and just as simple as the page state.

Enough Yapping, Show Me the Code

Alright, alright…

As for the backend—you can install and use Inertia’s packages just like before. They’ll work seamlessly with Vortex on the frontend. So we’ll focus on the frontend part. I’ll use Svelte in the examples (my preferred framework), but it should work with Vue, React, Angular, or Solid too.

Install Vortex

npm install @westacks/vortex --dev

For a progress indicator on page loads:

npm install @bprogress/core --dev

Root File

Let’s create the frontend entry point. For Svelte, it’ll be app.svelte.ts:

// app.svelte.ts
import { createVortex, install, subscribe } from '@westacks/vortex'
import inertia from '@westacks/vortex/inertia'
import bprogress from '@westacks/vortex/bprogress'
import { mount, hydrate } from 'svelte'
import { resolve } from './resolve'
import App from './App.svelte'

// Initializes custom Axios instance and reactive state for our page
createVortex(async (target, page, ssr) => {
    const h = ssr ? hydrate : mount

    install(inertia(page), bprogress()) // Adds extensions to the router

    let props = $state(await resolve(page))

    h(App, { target, props })

    // Subscribes to page changes
    subscribe(async (page) => Object.assign(props, await resolve(page)))
})

Your resolve function will look like this:

// resolve.ts
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import type { Component } from 'svelte'

export async function resolve(page) {
    const component = await resolvePageComponent(
        `./Pages/${page.component}.svelte`,
        import.meta.glob<Component>('./Pages/**/*.svelte')
    )

    return { component, props: page.props ?? {} }
}

Root component:

<!-- App.svelte -->
<script>
    let { component, props } = $props()
</script>

{#if component}
    <component.default {...props}></component.default>
{/if}

SSR Support

If you want SSR, create ssr.ts and SSR.svelte. (Yes, SSR.svelte is necessary — quirk of Inertia backends, as they don't render root element during SSR for some reason)

// ssr.ts
import { createVortexServer } from '@westacks/vortex/server'
import { render } from 'svelte/server'
import { resolve } from './resolve'
import SSR from './SSR.svelte'

createVortexServer(async (page) => {
    const { body, head } = render(SSR, { props: { page, ...await resolve(page) }})

    return { body, head: [head] }
})
<!-- SSR.svelte -->
<script lang="ts">
    import App from './App.svelte'
    const { component, props, page } = $props()
</script>

<div id="app" data-ssr="true" data-page="{JSON.stringify(page)}">
    <App {component} {props} />
</div>

Utility Module

To get a seamless experience with forms and links, create a helper module:

// Utils/inertia.ts
import { readable } from 'svelte/store'
import { subscribe, getPage, useForm as _useForm, link } from '@westacks/vortex'

// Link helper (written in Svelte's syntax, but should be possible to bind it to state changes with other frameworks too)
export const inertia = link

// Reactive page state store
export const page = readable(getPage(), subscribe)

// Reactive form store
export function useForm<T extends object>(rememberKeyOrData: string | T | (() => T), maybeData?: T | (() => T)) {
    const { get, subscribe } = _useForm(rememberKeyOrData, maybeData)

    const store = readable(get(), subscribe)
    // Prevent Svelte from trying to set the store (only writable can do so)
    // @ts-expect-error
    store.set = () => {}

    return store
}

Creating Pages

Create pages in the Pages directory as you would with Inertia:

<script>
    import { useForm, inertia, page } from '@/Utils/inertia'
		
    const { appName } = $props()
		
    const form = useForm({
        email: '',
        password: '',
    })
		
    function submit(e) {
        e.preventDefault()
        $form.post('/login').then(() => console.log('Logged In!'))
    }
</script>

<h1>{$page.props.appName}</h1>

<a href="/" use:inertia>Home</a>

<form on:submit={submit}>
    <input type="text" bind:value={$form.email} />
    <input type="password" bind:value={$form.password} />
    <button>Login</button>
</form>

That’s It?

Well, it’s hard to fit everything into one blog post. But as a proof of concept — I migrated this portfolio website from Inertia to Vortex, and it works, as you can see (I hope you can). The result library is less then 10kb gzipped and with only 1 required dependency. Check out the source code here and the library itself here.

Thanks for reading my rambling this far!

Comments

SableCat
SableCat
a month ago

I believe it should be possible to bind page state and form reactively to Vue using reactive helper. It even more straightforward with Solid.js, as you work with signals there as well. Looks very promising!

2025 © All rights reserved.