useAutoComputedState
A useComputedState that refreshes itself: it runs compute on the first build and again whenever keys change. It returns the same MutableComputedState<T>, so the value, refresh(), updateValue, and clear() all behave identically - the difference is that the computation is triggered for you.
class UserScreen extends HookWidget {
final String userId;
const UserScreen({super.key, required this.userId, required this.service});
final UserService service;
Widget build(BuildContext context) {
// Runs on the first build, and again whenever userId changes.
final userState = useAutoComputedState(() => service.load(userId), keys: [userId]);
return userState.value.when(
notInitialized: () => const SizedBox.shrink(),
inProgress: (_) => const CircularProgressIndicator(),
ready: (user) => Text(user.name),
failed: (error) => Text('Failed: $error'),
);
}
}
Signature
MutableComputedState<T> useAutoComputedState<T>(
Future<T> Function() compute, {
bool shouldCompute = true,
HookKeys keys = hookKeysEmpty,
Duration debounceDuration = Duration.zero,
bool isRetryable = false,
});
compute- the async computation, run on first build and on everykeyschange (subject toshouldCompute).shouldCompute(trueby default) - gates the computation. When it isfalse, the state is cleared immediately (back tonotInitialized) and any in-flight computation is cancelled. When it transitions back totrue,computeruns again.keys- a change to any key triggers a refresh. Pass the inputscomputedepends on.debounceDuration(Duration.zeroby default) - delay that must pass after akeyschange beforecomputefires. Useful for search-as-you-type.isRetryable(falseby default) - whentrue, errors are madeRetryableso consumers can retry. Note thatshouldComputemay have changed by the time a laterRetryable.retry()runs, which can re-runcomputeunder different conditions.
The return type is MutableComputedState<T> - see useComputedState for value, valueOrNull, refresh(), updateValue, and clear().
Use cases
-
Loading screen data when the screen opens, and reloading it when an input changes - the default tool for a one-shot read tied to the Screen / State / View pattern.
-
Search and filtering, where the query is a key and
debounceDurationrate-limits the fetch:class UserSearch extends HookWidget {const UserSearch({super.key, required this.service});final UserService service;Widget build(BuildContext context) {final query = useFieldState();// 300ms must pass between keystrokes before the search fires.final resultsState = useAutoComputedState(() => service.search(query.value),keys: [query.value],debounceDuration: const Duration(milliseconds: 300),);return Column(children: [TextField(onChanged: (it) => query.value = it),for (final user in resultsState.valueOrNull ?? const <User>[]) Text(user.name),],);}} -
Reads that must wait for a prerequisite (an authenticated user, a non-null id). Gate them with
shouldComputesocomputenever runs with invalid inputs:class ProfileScreen extends HookWidget {final String? userId;const ProfileScreen({super.key, required this.userId, required this.service});final UserService service;Widget build(BuildContext context) {// The compute never runs with a null id - it waits until one is available.final userState = useAutoComputedState(() => service.load(userId!),shouldCompute: userId != null,keys: [userId],);return Text(userState.valueOrNull?.name ?? 'Waiting for id');}}
For cursor-paginated lists, use usePaginatedComputedState instead - a single Future<T> can't model loadMore. For an ongoing Stream<T>, use useMemoizedStream.
Caveats
-
shouldCompute: falseclears the state. This is stricter than the paginated hook's gate - the moment it goesfalse,valuereturns tonotInitializedand the in-flight computation is cancelled. Use it for logical prerequisites (userId != null), not transient conditions like connectivity, where you would lose the loaded data on every blip. -
Use
keysfor reactive inputs, method calls for imperative actions. Don't bump auseState<int>counter just to force a refresh - the state already exposesrefresh().// DON'T - a counter in keys carries no information, only "something happened"final triggerState = useState(0);final dataState = useAutoComputedState(() => repo.fetch(query), keys: [query, triggerState.value]);onRefresh: () => triggerState.value++;// DO - imperative action -> method callfinal dataState = useAutoComputedState(() => repo.fetch(query), keys: [query]);onRefresh: () => dataState.refresh(); -
A refresh puts the state back into
inProgress, sovalueOrNullbecomesnulland a naive View blinks to its loader. To keep the previous data on screen during a refresh, wrap the value inusePreviousIfNull, or use theuseValueOrPrevious()extension on the state. -
Errors from the auto-triggered first load aren't awaited anywhere, so they surface as unhandled zone errors (the let-it-crash default). The failure also lands in
valueasfailed, so you can render it - but if you need toawaitthe failure (e.g. in a test), gate withshouldCompute: falseand callrefresh()manually. -
This hook pairs with the
ComputedStateWrapperandRefreshableComputedStateWrapperwidgets, which render thenotInitialized/inProgress/ready/failedbranches for you. Reach for them in the View instead of hand-writing awhen(...).
See also
- useComputedState - the on-demand version; this hook adds the auto-refresh
- usePaginatedComputedState - the paginated cousin for cursor-based lists
- useDebounced - debounce a value before feeding it into
keys - usePreviousIfNull - keep the last value visible across a refresh