Skip to main content

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, HttpContext
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(ctx: HttpContext):
    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, ctx=ctx)

Without a context 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(ctx: HttpContext):
    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:int}")
@inertia.page("Users/Show")
async def show(user_id: int):
    return {"user": await User.get(id=user_id)}

The function declares only what it uses. Ask for ctx (or context) by name and you get it; leave it out and it is 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(ctx: HttpContext):
    form = await ctx.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 context take one parameter; those that do not, take none.

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

props itself can be a callable returning the whole mapping:

await inertia.render("Home", lambda ctx: {"path": ctx.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

The adapter targets Sillo v1. Handlers take one argument, the HttpContext; there is no Request/Response pair.

# before
async def home(request: Request, response: Response):
    return await inertia.render(request, response, "Home", {"name": "Sillo"})

# after
async def home(ctx: HttpContext):
    return await inertia.render("Home", {"name": "Sillo"})

render and redirect take the component or location first and nothing else positionally; pass a non-current context as ctx=. Inertia.location() is synchronous — drop the await. Props callbacks that took a request take a ctx now, and the parameter is optional — lambda _: value becomes lambda: value.

Metadata

Release files for sillo-inertia 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sillo-inertia 1.0.0
File Size Uploaded
sillo_inertia-1.0.0.tar.gz 112.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sillo-inertia 1.0.0
File Interpreter ABI Platform
sillo_inertia-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 136.5 kB

Release files / sillo_inertia-1.0.0.tar.gz

Download URL sillo_inertia-1.0.0.tar.gz
Size 112.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f88c926a10280c7c44a5a6cab431c467ada8b4638ff81174cb20664231ad039a
BLAKE2b-256 checksum
How to use checksums
71809f75d0848f9e986692c2d91151b1353d0735457fc307fecb6259e1192203
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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}

Release files / sillo_inertia-1.0.0-py3-none-any.whl

Download URL sillo_inertia-1.0.0-py3-none-any.whl
Size 23.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f02cb6827dbff96f1f490dd90ac44434568003a81a687f9225f23cfe5cf5efc3
BLAKE2b-256 checksum
How to use checksums
33416c6063ea56103eed308ee0b318d138c13a9ff1be5c1fab68823f9a0147a2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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}
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