$data
Load data for screens and layouts with automatic caching.
Screen Loaders
Create a +screen.load.wh file next to your screen:
// routes/user/[id]/+screen.load.wh
suspend fun load(): User {
val userId = $screen.params.id
return $fetch("https://api.example.com/users/$userId")
}
// routes/user/[id]/+screen.wh
<Column p={16}>
<Text fontSize={24}>{$data.name}</Text>
<Text>{$data.email}</Text>
<Text>{$data.bio}</Text>
</Column> How It Works
- Loader runs before screen renders
- Screen shows loading state automatically
- Access loaded data via
$data - Data is cached by route + params
Layout Loaders
A +layout.load.wh's fields join every screen's $data below it, the
way SvelteKit's page.data merges the chain: outermost layout first, the
screen's own loader last. There is one $data, not a second name for the layout's
half.
// routes/+layout.load.wh
data class AppData(val user: User, val notifications: List<Notification>)
suspend fun load(): AppData {
return AppData(
user = $fetch("https://api.example.com/me"),
notifications = $fetch("https://api.example.com/notifications")
)
}
// Any screen under this layout
<Column>
<Text>Welcome, {$data.user.name}!</Text>
<Text>You have {$data.notifications.size} notifications</Text>
</Column> A field name two loaders in the same chain both declare is a build error naming both files.
A merged loader's return type has to be a data class declared in its own .load.wh.
Deferred Loading
Load critical data first, defer non-critical data:
// +screen.load.wh
data class ShowData(
val show: Show,
val episodes: List<Episode>?
)
suspend fun load(): ShowData {
val show = $fetch.get(url = "https://api.example.com/shows/$id")
val episodes = $defer {
$fetch.get(url = "https://api.example.com/shows/$id/episodes")
}
return ShowData(show, episodes)
}
// +screen.wh
<Column>
<Text>{$data.show.name}</Text>
@if ($data.episodes != null) {
@for (ep in $data.episodes) {
<Text>{ep.title}</Text>
}
} else {
<CircularProgressIndicator />
}
</Column> The screen renders immediately with show data, then recomposes when episodes loads.
Invalidation
Force a loader to run again:
$invalidate() // this route
$invalidate("/show/${id}") // that route, wherever it sits on the stack
$invalidate.all() // everything A route invalidated while it is off the stack just loads fresh next time it is shown:
<Button onClick={() => $invalidate()}>
Refresh
</Button> Automatic Caching
Data is automatically cached by route + parameters:
- Navigate to
/user/123→ loader runs, data cached - Navigate away, then back to
/user/123→ cached data shown instantly - Navigate to
/user/456→ loader runs with new params - Call
$invalidate()→ cache cleared, loader re-runs
Using Route State
Combine with navigation state for instant previews:
// From list screen
$navigate("/show/${show.id}", state = {
name: show.name,
poster: show.poster
})
// In loader - check for preview data
suspend fun load(): Show {
val preview = $screen.state
if (preview != null) {
// Show preview immediately, load full data in background
}
return $fetch("https://api.example.com/shows/${$screen.params.id}")
} Error Handling
Let the loader throw. The nearest +error.wh at or above the screen renders in
its place:
// +screen.load.wh
suspend fun load(): User {
return $fetch("https://api.example.com/users/${$screen.params.id}")
}
// +error.wh
<Column class="p-16 gap-8 safe">
<Text class="text-lg font-bold">Something went wrong</Text>
<Text class="text-muted-foreground">{$screen.error.message}</Text>
<Button onClick={() => $invalidate()}>Try again</Button>
</Column> With no +error.wh in the tree, a failure shows the message and a Retry button.
A layout loader's failure goes to the +error.wh above that layout's own
folder, since the one beside it would render inside the layout that failed.
Multiple Data Sources
data class ProfileData(
val user: User,
val posts: List<Post>,
val followers: Int
)
suspend fun load(): ProfileData {
val userId = $screen.params.id
val user = $fetch("https://api.example.com/users/$userId")
val posts = $fetch("https://api.example.com/users/$userId/posts")
val followers = $fetch("https://api.example.com/users/$userId/followers/count")
return ProfileData(user, posts, followers)
} Use loaders for data that's needed before the screen renders. For data triggered by user actions (like form submissions), use regular suspend functions.
See Also
- $fetch - HTTP requests
- $screen - Route parameters
- $onMount / $onDispose - Lifecycle hooks