Skip to content

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

docs/examples/inertia/props_and_reloads.py
"""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:

resources/pages/Analytics/Dashboard.tsx
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>
  );
}
resources/pages/Analytics/Dashboard.vue
<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 in onceProps: {"countries": {"prop": "countries"}}. On subsequent Inertia navigations, the client sends X-Inertia-Except-Once-Props: countries so the server skips re-evaluating load_countries. Explicitly requesting only: ["countries"] forces a refresh.
  • defer("catalog", load_catalog).once(): Defers the expensive query until after initial paint, then caches the resolved value in onceProps across subsequent page navigations.