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()
We added reloadSilently because users hate seeing a loading spinner flash when the data refresh takes 200ms. If the reload is fast enough, they never notice it happened.

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() }
                    }
                }
            }
        }
    }
}