Shared Data & Flash Messages¶
Cross-cutting concerns like the currently authenticated user, organization permissions, and toast notifications (flash) are shared across pages without repeating props in every route handler.
Per-Request Shared Props (share)¶
Call share(connection, key, value) inside route handlers, guards, dependencies, or middleware to attach props to the current request's Inertia envelope:
"""Inertia.js v2 shared session props, flash messages, validation errors, and Precognition."""
from __future__ import annotations
from typing import Annotated, Any
from litestar import Litestar, Request, post
from litestar.params import Body
from msgspec import Struct
from litestar_vite import InertiaConfig, RuntimeConfig, ViteConfig, VitePlugin
from litestar_vite.inertia import error, flash, precognition, share
class AuthenticatedUser(Struct, frozen=True, gc=False):
"""Typed current user model registered in ``shared_page_prop_types``."""
id: int
email: str
role: str
class CreateProjectPayload(Struct, frozen=True, gc=False):
"""Form payload validated by both live Precognition and final submission."""
name: str
slug: str
@post("/projects", component="Projects/Index", sync_to_thread=False)
@precognition
def create_project(request: Request[Any, Any, Any], data: Annotated[CreateProjectPayload, Body()]) -> dict[str, Any]:
"""Validate and create a project with Precognition and flash notifications."""
share(request, "currentUser", AuthenticatedUser(id=7, email="ada@example.com", role="admin"))
if data.slug == "reserved":
error(request, "slug", "The 'reserved' slug is unavailable.")
flash(request, "Please choose a different project slug.", category="error")
return {"created": False}
flash(request, f"Project '{data.name}' created.", category="success")
return {"created": True, "slug": data.slug}
def create_app() -> Litestar:
"""Create a Litestar app with typed shared props and Precognition enabled."""
vite_config = ViteConfig(
mode="hybrid",
dev_mode=True,
inertia=InertiaConfig(
root_template="index.html",
precognition=True,
extra_static_page_props={"environment": "staging"},
shared_page_prop_types={"currentUser": AuthenticatedUser},
),
runtime=RuntimeConfig(start_dev_server=False),
)
return Litestar(route_handlers=[create_project], plugins=[VitePlugin(config=vite_config)])
def main() -> None:
"""Verify ``InertiaConfig`` shared prop typing and Precognition registration."""
app = create_app()
plugin = app.plugins.get(VitePlugin)
assert isinstance(plugin.config.inertia, InertiaConfig)
assert plugin.config.inertia.precognition is True
assert plugin.config.inertia.shared_page_prop_types == {"currentUser": AuthenticatedUser}
__all__ = ("AuthenticatedUser", "CreateProjectPayload", "create_app", "create_project", "main")
Lazy Shared Props¶
You can pass prop wrappers (lazy(), once(), defer(), always()) to share() so expensive shared data is only evaluated when needed or cached across visits:
from typing import Any
from litestar import Request
from litestar_vite.inertia import lazy, once, share
def attach_shared_context(request: Request[Any, Any, Any]) -> None:
"""Attach cached permissions and on-demand notification counts."""
share(request, "permissions", once("permissions", lambda: ["orders:read", "orders:write"]))
share(request, "unreadCount", lazy("unreadCount", lambda: 5))
Static & Session-Backed Shared Props¶
In addition to calling share() per request, InertiaConfig supports declarative static and session-backed props:
| Option | Type | Behavior |
|---|---|---|
extra_static_page_props |
dict[str, Any] |
Merged into every Inertia response (e.g., {"appEnv": "production"}). |
extra_session_page_props |
set[str] |
Keys automatically read from request.session and included in props when present. |
| Built-in Shared Props | errors, csrf_token |
Automatically injected on every Inertia response (errors defaults to {}). |
Note
During partial reloads (only / except), shared props are filtered by the client's requested keys just like handler props — unless wrapped in always().
Typing Shared Props¶
Props registered dynamically via share(connection, key, value) inside guards or middleware have no static return annotation on individual route handlers.
By declaring InertiaConfig(shared_page_prop_types={"currentUser": AuthenticatedUser}), Litestar Vite's OpenAPI/TypeScript generator registers AuthenticatedUser in the shared schema registry and emits the exact TypeScript type on SharedPageProps in page-props.ts — eliminating unknown or hand-maintained duplicate interfaces.
Top-Level Flash Messages¶
Inertia v2.3+ places flash messages at the top level of the page object (page.flash) rather than inside page.props.flash so flash messages are never accidentally cached in window.history.state or re-triggered on Back/Forward navigation.
Call flash(connection, message, category="info") on the server:
from typing import Any
from litestar import Request, post
from litestar_vite.inertia import InertiaRedirect, flash
@post("/settings", sync_to_thread=False)
def save_settings(request: Request[Any, Any, Any]) -> InertiaRedirect:
"""Persist settings and stage a top-level flash message across the redirect."""
flash(request, "Settings saved.", category="success")
return InertiaRedirect(request, redirect_to="/settings")
Reading Flash Messages on the Client¶
When TypeGenConfig runs (with InertiaTypeGenConfig(include_default_flash=True)), Litestar Vite generates src/generated/inertia.d.ts augmenting Inertia's FlashDataType with FlashMessages (success?: string[], error?: string[], info?: string[], warning?: string[]):
import { usePage } from "@inertiajs/react";
export function ToastBanner() {
const page = usePage() as { flash?: Record<string, string[]> };
const messages = page.flash?.success ?? [];
if (!messages.length) return null;
return (
<div role="status" className="toast-success">
{messages.map((msg, idx) => (
<p key={idx}>{msg}</p>
))}
</div>
);
}
<script setup lang="ts">
import { computed } from "vue";
import { usePage } from "@inertiajs/vue3";
const page = usePage() as unknown as { flash?: Record<string, string[]> };
const messages = computed(() => page.flash?.success ?? []);
</script>
<template>
<div v-if="messages.length" role="status" class="toast-success">
<p v-for="(msg, idx) in messages" :key="idx">{{ msg }}</p>
</div>
</template>