Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

sillo-inertia

An Inertia.js adapter for Sillo, with React and Vue.

A handler names a component and returns its props. Whether that becomes a full HTML document or a JSON page object is the adapter's problem, not yours — it depends on whether the client has booted yet, which is a property of the request rather than of your code.

from sillo import SilloApp
from sillo.core.http import Request, Response
from sillo_inertia import Inertia, vite_react

app = SilloApp()
inertia = Inertia(
    app,
    root_view="resources/views/app.html",
    version="1",
    vite=vite_react(dev=True),
)


@app.get("/")
async def home(request: Request, response: Response):
    return await inertia.render("Home", {"name": "Sillo"})

Install

pip install sillo-inertia

Or with uv:

uv add sillo-inertia

The root view

Your resources/views/app.html needs the {{ inertia }} placeholder:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    {{ inertia_head }}
  </head>
  <body>
    <div id="{{ root_id }}"></div>
    {{ inertia }}
  </body>
</html>

{{ inertia }} expands to a <script type="application/json" data-page="app"> element holding the page object — that is where Inertia 2.x and later read it from. Do not put it on the root <div> as data-page: that was the 1.x convention, current clients never look there, and the page boots with a null page object and throws Cannot read properties of null (reading 'component'). If you are targeting a 1.x client, {{ inertia_page }} still gives you the HTML-escaped attribute value.

Rendering

render

await inertia.render(
    "Users/Index",                     # the component the client resolves
    {"users": [...]},                  # props
    status_code=200,
    headers={"X-Total-Count": "42"},
    view_data={"title": "Users"},      # placeholders in the root view
)

render returns a response. Return it from the handler; there is no response object to fill in and nothing to pass along.

The request it answers is the one the middleware is currently handling. If you need a different one — a background job, a test that calls a function directly — pass it by keyword:

await inertia.render("Home", props, request=request)

Without a request from either source, render raises OutsideRequestError and tells you which of those two cases you are in.

Without the adapter in scope

render, redirect, back and location are importable on their own. They find the adapter through the same middleware, which means a routes module can build Inertia responses without importing the module that owns the app — and so without the circular import that would otherwise cause.

from sillo_inertia import render


@app.get("/")
async def home(request: Request, response: Response):
    return await render("Home", {"name": "Sillo"})

As a decorator

When a handler does nothing but produce props, @inertia.page takes the rest:

@app.get("/users/{user_id}")
@inertia.page("Users/Show")
async def show(user_id):
    return {"user": await User.get(id=user_id)}

The function declares only what it uses. Ask for request or response by name and you get them; leave them out and they are not passed. Path parameters and injected dependencies arrive as usual.

Returning a response instead of a mapping sends that response untouched, so a handler can still redirect out of a page:

@app.post("/users")
@inertia.page("Users/Create")
async def create(request: Request):
    form = await request.json()
    await User.create(**form)
    return inertia.redirect("/users")

Anything you would pass to render can be pinned on the decorator:

@inertia.page("Errors/NotFound", status_code=404)

Props

A prop can be a value, a callable, or a coroutine function. Callables that want the request take one parameter; those that do not, take none.

{
    "count": 5,                                   # a value
    "total": lambda: Order.count(),               # called per request
    "mine": lambda request: request.user.orders,  # given the request
    "stats": fetch_stats,                         # async, awaited
}

props itself can be a callable returning the whole mapping:

await inertia.render("Home", lambda request: {"path": request.url.path})

Lazy props

A lazy prop is left out of a partial reload unless the client asks for it by name, so the work behind it is skipped on requests that would discard it:

from sillo_inertia import lazy

await inertia.render(
    "Posts/Show",
    {
        "post": post.to_dict(),
        "comments": lazy(lambda: Comment.for_post(post.id)),
    },
)

On a partial reload — X-Inertia-Partial-Component: Posts/Show and X-Inertia-Partial-Data: comments — only comments is resolved and returned.

Note that this is narrower than lazy in Inertia's own server adapters, where a lazy prop is excluded from every regular visit and included only when asked for by name. Here it is resolved on a full visit like any other prop, and only partial reloads filter it out. Do not rely on it to keep an expensive query off the first page load.

Shared props

Props every page gets. Set them once, at startup:

inertia.share(app_name="MyApp", auth={"user": None})

A page's own props win on a name clash.

Redirects

inertia.redirect("/dashboard")        # 303 after POST/PUT/PATCH, 302 after GET
inertia.redirect("/home", status_code=301)
inertia.back()                        # to the Referer
inertia.back(fallback="/posts")       # ...when there is no Referer

The 303 is not decoration. On a 302 the browser repeats the POST against the new URL, so a redirect after a successful create creates a second record.

location sends the client out of Inertia entirely — a full browser visit rather than an XHR one, which is the only way to reach an external URL:

inertia.location("https://billing.example.com/checkout")

Configuration

Inertia(
    app=app,                            # attaches the middleware
    root_view="resources/views/app.html",
    version="1.0.0",                    # a string, or a callable returning one
    root_id="app",                      # the mount point's element id
    base_dir=".",                       # where asset paths resolve from
    vite=vite_react(dev=True),
)

The adapter can also be attached later:

inertia = Inertia(root_view="resources/views/app.html")
inertia.middleware(app)

Asset versions

When the client's X-Inertia-Version does not match the current one, the middleware answers with a 409 and X-Inertia-Location before the handler runs. The client does a full visit and comes back on the current build. A version of None disables the check.

Passing a callable re-reads it per request, which is what you want when the version comes from a build manifest that changes without a restart.

Vite

vite_react(
    entry="src/main.jsx",
    dev_server="http://localhost:5173",
    manifest_path="dist/.vite/manifest.json",
    asset_prefix="/assets/",
    dev=True,
    react_refresh=True,
)

vite_vue(
    entry="src/main.ts",
    dev_server="http://localhost:5173",
    manifest_path="dist/.vite/manifest.json",
    asset_prefix="/assets/",
    dev=True,
)

{{ inertia_head }} renders the tags: the dev server's client and entry in development, the hashed manifest entries and their CSS in production.

Upgrading from 0.0.x

render and redirect no longer take request and response:

# before
return await inertia.render(request, response, "Home", {"name": "Sillo"})
# after
return await inertia.render("Home", {"name": "Sillo"})

The old call raises a TypeError naming the new form, so nothing fails silently. Inertia.location() is now synchronous — drop the await.

Props callbacks taking a request still work unchanged; the parameter is now optional rather than required, so lambda _: value can become lambda: value.

Project Links

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sillo_inertia-0.1.0.dev1.tar.gz (112.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sillo_inertia-0.1.0.dev1-py3-none-any.whl (24.0 kB view details)

Uploaded Python 3

File details

Details for the file sillo_inertia-0.1.0.dev1.tar.gz.

File metadata

  • Download URL: sillo_inertia-0.1.0.dev1.tar.gz
  • Upload date:
  • Size: 112.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sillo_inertia-0.1.0.dev1.tar.gz
Algorithm Hash digest
SHA256 c115adacf3d322f56dbd3b8fa058b6cd6d8f18b962019790f29e9784b7357a90
MD5 b991bf60cc7639c38c191238fbb911c9
BLAKE2b-256 ba41218084d27b62e820b81b3cd2dc9a7cd6034046a78c4af8fad3c262e8e358

See more details on using hashes here.

File details

Details for the file sillo_inertia-0.1.0.dev1-py3-none-any.whl.

File metadata

  • Download URL: sillo_inertia-0.1.0.dev1-py3-none-any.whl
  • Upload date:
  • Size: 24.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sillo_inertia-0.1.0.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 c01301ee6641344326145ea7ba80113674368e282f464e7af4dd9b9e05031453
MD5 eb6a2ab4c2a8f38087be4fdc8103b429
BLAKE2b-256 734c4926019ebec4cafc5bebbed3154d74b9dc8731ecf8b1c99dc0ec5def67c0

See more details on using hashes here.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page