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