Data Loading
Load async data with built-in loading, loaded, and error states.
The DataLoader wraps an async operation and tracks its state. You define what to load, and it gives
you a sealed state you can render against — no manual boolean flags.
Basic usage
Create a loader in your component with dataLoader. It starts loading immediately on mount:
class UserList(ctx: NoProps) : PureComponent(ctx) {
private val loader = dataLoader {
flow {
val users = api.fetchUsers()
emit(users)
}
}
override fun VDom.render() {
loader(this) {
loading {
ui.loading.segment { +"Loading users..." }
}
loaded { users ->
ui.list {
users.forEach { user ->
noui.item { +user.name }
}
}
}
error { err ->
ui.error.message { +"Failed: ${err.message}" }
}
}
}
} The loader(this) call renders the appropriate block based on the current state. Only one block runs
at a time.
Fixed values
When you already have the data and don't need to load it asynchronously, use dataLoaderOf:
private val loader = dataLoaderOf(listOf("Alice", "Bob", "Charlie")) This creates a loader that starts in the Loaded state immediately.
Reloading
Trigger a reload from a button click or any event. Two variants:
// Standard reload — resets to Loading state, shows loading UI
loader.reload()
// Silent reload — refetches without showing loading state
loader.reloadSilently() Both methods accept an optional debounce in milliseconds (default: 200ms):
// Debounce rapid reloads (e.g. from a search input)
loader.reload(debounceMs = 500)
loader.reloadSilently(debounceMs = 500) State inspection
Check the current state programmatically:
loader.isLoading() // true during initial load or after reload()
loader.isLoaded() // true when data is available
loader.isError() // true when the flow threw an exception
// Negated versions for convenience
loader.isNotLoading()
loader.isNotLoaded()
loader.isNotError() Modifying loaded data
Update the loaded value without triggering a full reload:
// Transform the current value
loader.modifyValue { users ->
users.filter { it.isActive }
}
// Replace the value entirely
loader.setLoaded(newUsers)
// Set an arbitrary state
loader.setState(DataLoader.State.Loading()) Stream access
The loader exposes its state and value as Streams, so you can subscribe to changes or compose with other reactive sources:
// The full state (Loading | Loaded | Error)
val stateStream: Stream<DataLoader.State<T>> = loader.state
// Just the loaded value (null when not loaded)
val valueStream: Stream<T?> = loader.value Full example
A component that loads data, shows a refresh button, and handles all three states:
class ProductList(ctx: NoProps) : PureComponent(ctx) {
private val loader = dataLoader {
flow { emit(api.fetchProducts()) }
}
override fun VDom.render() {
ui.segment {
ui.blue.button {
onClick { loader.reloadSilently() }
icon.sync_alternate.render()
+"Refresh"
}
loader(this) {
loading {
ui.active.centered.inline.loader {}
}
loaded { products ->
ui.relaxed.divided.list {
products.forEach { product ->
noui.item {
noui.content {
noui.header { +product.name }
noui.description { +product.description }
}
}
}
}
}
error { err ->
ui.error.message {
noui.header { +"Could not load products" }
p { +err.message.orEmpty() }
}
}
}
}
}
}