Partial Reloads & Deferred Props¶
Inertia gives you fine-grained control over when each prop is evaluated on the server. Choosing the right prop wrapper keeps initial page loads instantaneous while avoiding manual fetch() state management.
Prop Evaluation Matrix¶
| Helper | Initial Load | Partial Reload | Client Behavior |
|---|---|---|---|
| Plain value | Included | Included unless filtered | Replaces prop value |
always |
Always included | Always included (bypasses only / except_) |
Replaces prop value on every visit (auth, errors) |
defer |
Queued in deferredProps |
Auto-fetched by <Deferred> after mount |
Loaded in grouped background requests after initial paint |
once |
Included | Skipped once cached (only forces refresh) |
Cached across client page navigations (onceProps) |
optional |
Excluded | Only when listed in only |
Designed for <WhenVisible> viewport loading |
lazy |
Excluded | Only when listed in only |
Classic on-demand partial reload prop |
merge |
Included | Appends, prepends, or deep-merges | Combines items in place during partial reloads (mergeProps) |
Complete Server Example¶
"""Inertia.js v2 deferred props, partial reloads, once caching, and infinite scroll."""
from __future__ import annotations
from typing import Any
from litestar import Litestar, get
from litestar_vite import InertiaConfig, RuntimeConfig, ViteConfig, VitePlugin
from litestar_vite.inertia import always, defer, lazy, merge, once, optional, scroll_props
def _load_slow_analytics() -> dict[str, int]:
"""Simulate an expensive analytics query deferred until after initial render."""
return {"weeklyActive": 1420, "conversionRateBps": 385}
def _load_audit_log() -> list[str]:
"""Simulate an on-demand audit trail loaded only when requested."""
return ["user.login", "invoice.exported"]
@get("/analytics", component="Analytics/Dashboard", sync_to_thread=False)
def analytics_dashboard() -> dict[str, Any]:
"""Combine eager, deferred, once-cached, optional, and merged props."""
return {
"permissions": always("permissions", {"canExport": True}),
"summary": {"totalRevenue": 94200},
"metrics": defer("metrics", _load_slow_analytics, group="charts"),
"featureCatalog": once("featureCatalog", lambda: ["sso", "audit_stream"]),
"auditLog": optional("auditLog", _load_audit_log),
"legacySummary": lazy("legacySummary", lambda: {"archived": 12}),
"events": merge(
"events",
[{"id": 1, "title": "Deployment succeeded"}, {"id": 2, "title": "Backup completed"}],
strategy="append",
match_on="id",
),
"scroll": scroll_props(page_name="page", current_page=1, next_page=2),
}
def create_app() -> Litestar:
"""Create a Litestar app demonstrating Inertia v2 prop wrappers."""
vite_config = ViteConfig(
mode="hybrid",
dev_mode=True,
inertia=InertiaConfig(root_template="index.html"),
runtime=RuntimeConfig(start_dev_server=False),
)
return Litestar(route_handlers=[analytics_dashboard], plugins=[VitePlugin(config=vite_config)])
def main() -> None:
"""Verify Inertia v2 prop wrapper construction."""
app = create_app()
assert app is not None
deferred = defer("metrics", _load_slow_analytics, group="charts")
merged = merge("events", [{"id": 1}], strategy="append", match_on="id")
assert deferred.group == "charts"
assert merged.strategy == "append"
assert merged.match_on == ["id"]
__all__ = ("analytics_dashboard", "create_app", "main")
Important
Avoid the False-Lazy Pitfall: Always pass a callable reference (defer("metrics", _load_slow_analytics) or lazy("report", lambda: build_report(org_id))) rather than calling the function eagerly (defer("metrics", _load_slow_analytics())). Both synchronous and async def callables are supported; async callables are resolved concurrently on the event loop by InertiaResponse.
Client-Initiated Partial Reloads¶
Use router.reload({ only: [...] }) or router.reload({ except: [...] }) to refresh a subset of props on the current page without re-evaluating unrelated database queries:
import { router } from "@inertiajs/react";
// Refresh only the "metrics" and "notifications" props
router.reload({ only: ["metrics", "notifications"] });
// Refresh everything except the heavy "catalog" prop
router.reload({ except: ["catalog"] });
// Reset merged arrays before reloading
router.reload({ only: ["feed"], reset: ["feed"] });
Deferred Props & Grouped Resolution¶
Props wrapped in defer(key, fn, group="default") are omitted from the initial response and grouped in deferredProps: { default: ["metrics"], charts: ["revenue", "retention"] }. Props in the same group are fetched together in a single background request after the page mounts:
import { Deferred, WhenVisible } from "@inertiajs/react";
export default function AnalyticsDashboard() {
return (
<div>
<Deferred data="metrics" fallback={<div>Loading metrics...</div>}>
<MetricsChart />
</Deferred>
<WhenVisible data="audit_log" buffer={200} fallback={<div>Scrolling into view...</div>}>
<AuditLogTable />
</WhenVisible>
</div>
);
}
<script setup lang="ts">
import { Deferred, WhenVisible } from "@inertiajs/vue3";
</script>
<template>
<Deferred data="metrics">
<template #fallback>
<div>Loading metrics...</div>
</template>
<MetricsChart />
</Deferred>
<WhenVisible data="audit_log" :buffer="200">
<template #fallback>
<div>Scrolling into view...</div>
</template>
<AuditLogTable />
</WhenVisible>
</template>
Viewport-Triggered Loading (optional + <WhenVisible>)¶
Wrap below-the-fold data in optional("audit_log", load_audit_log) on the server and <WhenVisible data="audit_log" buffer={200}> on the client. An IntersectionObserver triggers a partial reload (only: ["audit_log"]) when the element approaches within 200px of the viewport. Pass always (<WhenVisible data="audit_log" always>) to re-fetch every time the element enters the viewport.
Client-Cached Props (once) & Fluent Chaining¶
once("countries", load_countries): Evaluated on the first visit and advertised inonceProps: {"countries": {"prop": "countries"}}. On subsequent Inertia navigations, the client sendsX-Inertia-Except-Once-Props: countriesso the server skips re-evaluatingload_countries. Explicitly requestingonly: ["countries"]forces a refresh.defer("catalog", load_catalog).once(): Defers the expensive query until after initial paint, then caches the resolved value inoncePropsacross subsequent page navigations.