Skip to content

Async data states

use_async_state comes from a page-loading pattern that appears repeatedly in TRNovel: start a request, show loading, write data on success, and write error on failure. The framework keeps only the generic skeleton; it does not add business copy, debounce behavior, or Tokio timer policy.

async state demo

This example refreshes data several times and simulates a failure on the fourth request. When a request fails, the old data remains visible; the error only describes the current failed request.

cargo run --example async_state

Press r to refresh data and q to exit.

Changing dependencies starts a new future in use_async_state and writes the result back into three State handles.
let result = hooks.use_async_state(
    move || async move {
        tokio::time::sleep(Duration::from_millis(700)).await;

        if request_id % 4 == 3 {
            return Err(format!("request #{} failed", request_id + 1));
        }

        Ok::<_, String>(
            (1..=5)
                .map(|index| format!("task {} from request #{}", index, request_id + 1))
                .collect::<Vec<_>>(),
        )
    },
    request_id,
);

if result.loading.get() {
    // render loading row
}

if let Some(error) = result.error.read().as_ref() {
    // render error row
}

if let Some(items) = result.data.read().as_ref() {
    // render data rows
}

use_async_state returns three reactive handles:

  • data: State<Option<T>>
  • loading: State<bool>
  • error: State<Option<E>>

When the request_id dependency changes, the old future is replaced and the new future sets loading to true. On success it writes data; on failure it writes error. Either way, it sets loading back to false at the end.

The default behavior keeps old data while a refresh is running. That is useful for terminal lists: the user can still see the previous result, while loading and error clearly describe the current request state. A failure is not an empty list; a failure means “this refresh did not produce a new result.”

If your application needs to clear old data during refresh, compose your own state policy around the hook, or add an explicit reset option in the future. The default avoids flicker in terminal interfaces.

Do not call tokio::spawn at the top level of a component function and then write state from it. That bypasses dependency comparison and may start a new task every frame.

Next, read Input layers to understand why modals and search inputs no longer compete with background components for the same events.