Skip to main content

useComputedState

Drives a single async computation that runs on demand. It returns a MutableComputedState<T> whose value moves through notInitialized -> inProgress -> ready/failed, and nothing runs until you call refresh().

class ArticleScreen extends HookWidget {
final String articleId;

const ArticleScreen({super.key, required this.articleId, required this.service});

final ArticleService service;


Widget build(BuildContext context) {
// Nothing runs yet - compute fires only when refresh() is called.
final articleState = useComputedState(() => service.load(articleId));

return articleState.value.when(
notInitialized: () => ElevatedButton(
onPressed: articleState.refresh, // <- Starts the computation
child: const Text('Load'),
),
inProgress: (_) => const CircularProgressIndicator(),
ready: (article) => Text(article.title),
failed: (error) => Text('Failed: $error'),
);
}
}

Signature

MutableComputedState<T> useComputedState<T>(Future<T> Function() compute, {bool isRetryable = false});
  • compute - the async computation. It is not called until refresh() is invoked.
  • isRetryable (false by default) - when true, errors thrown by compute are made Retryable via Retryable.make, so consumers can call Retryable.tryGet(error)?.retry() to re-run the computation.

MutableComputedState<T> exposes:

  • value - a ComputedStateValue<T> sum type: notInitialized, inProgress, ready(value), or failed(exception). Read it with .when(...) / .maybeWhen(...). It never throws.
  • valueOrNull - the value when ready, otherwise null.
  • refresh() - if value is inProgress, awaits the running computation; otherwise starts a new one. Returns a Future<T>.
  • updateValue(T value) - set value to ready(value) directly, without running compute. Cancels any in-flight computation.
  • clear() - reset to notInitialized. Cancels any in-flight computation.
  • isInitialized - true once value is ready.

Use cases

  • A load that should start on an explicit user action rather than on build - a "Load" button, a lazily expanded section, a tab opened for the first time.
  • Per-item async that caches its result, where the item decides when to fetch (e.g. an expandable list tile that loads its detail on first expand).

If the computation should instead run automatically on the first build and re-run when its inputs change, reach for useAutoComputedState - it wraps this hook with that behavior.

Reading and driving the state

value is a sum type, not the loaded data. Pattern-match it to render every state explicitly, or use valueOrNull when you only care about the ready value:

// DON'T - .value is a ComputedStateValue<T>, not a T
final article = articleState.value; // <- This is the sum type, not the Article

// DO
final article = articleState.valueOrNull; // <- Article? - null unless ready
final widget = articleState.value.when(
notInitialized: () => loadButton,
inProgress: (_) => spinner,
ready: (article) => content(article),
failed: (error) => errorView(error),
);

The three mutators let you steer the state without re-fetching:

class ArticleEditor extends HookWidget {
final String articleId;

const ArticleEditor({super.key, required this.articleId, required this.service});

final ArticleService service;


Widget build(BuildContext context) {
final articleState = useComputedState(() => service.load(articleId));

return Column(
children: [
Text(articleState.valueOrNull?.title ?? 'No article'), // <- null until ready
TextButton(
onPressed: () => articleState.updateValue(const Article('Edited locally')), // <- set ready without a fetch
child: const Text('Edit'),
),
TextButton(
onPressed: articleState.clear, // <- back to notInitialized, cancels any in-flight compute
child: const Text('Discard'),
),
],
);
}
}

Caveats

  • value is never the data directly. Reading .value and treating it as T is the most common mistake - it is a ComputedStateValue<T>. Use .valueOrNull or .when(...) / .maybeWhen(...).

  • updateValue does not cancel a computation the way you might expect to combine it. It cancels any in-flight compute, then sets ready. If you call refresh() afterwards, a fresh compute runs and its result replaces what you set.

  • Don't shadow a computed state with a parallel useState. If you need to override the loaded value after a local edit or a mutation, call updateValue, not a separate useState<T?> that you read first.

    // DON'T - duplicated source of truth, drifts from refresh()
    final articleState = useComputedState(() => service.load(id));
    final overrideState = useState<Article?>(null);
    final current = overrideState.value ?? articleState.valueOrNull;

    // DO - single source of truth
    final articleState = useComputedState(() => service.load(id));
    final current = articleState.valueOrNull;
    onEdited: (article) => articleState.updateValue(article);
  • refresh() joins an in-flight computation rather than stacking a second one, so calling it from several places is safe. The returned future throws ComputedStateRefreshCancelled if the computation is cancelled (by clear() or updateValue) before it completes - await it only when you are prepared to handle or swallow that.

See also

  • useAutoComputedState - the same state, refreshed automatically on first build and on keys changes
  • useSubmitState - for writes/mutations rather than reads
  • useMemoized - for synchronous derivations that don't need a loading state
  • Common hooks - computed state in the context of the wider hook set