Documentation
$data / $layoutData
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
Load data shared across multiple screens:
// routes/+layout.load.wh
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, {$layoutData.user.name}!</Text>
<Text>You have {$layoutData.notifications.size} notifications</Text>
</Column> Deferred Loading
Load critical data first, defer non-critical data:
// +screen.load.wh
data class ShowData(
val show: Show,
val episodes: Deferred<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 reload the current screen's data:
<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 = $route.state
if (preview != null) {
// Show preview immediately, load full data in background
}
return $fetch("https://api.example.com/shows/${$screen.params.id}")
} Error Handling
// +screen.load.wh
data class Result(
val data: User?,
val error: String?
)
suspend fun load(): Result {
try {
val user = $fetch("https://api.example.com/users/${$screen.params.id}")
return Result(user, null)
} catch (e: Exception) {
return Result(null, e.message)
}
}
// +screen.wh
@if ($data.error != null) {
<Text color="#ef4444">Error: {$data.error}</Text>
} else if ($data.data != null) {
<Text>{$data.data.name}</Text>
} 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
- $route / $screen - Route parameters
- $onMount / $onDispose - Lifecycle hooks