Documentation

$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