useAsyncData<T, W> function

(ReadonlyRef<AsyncValue<T>>, void Function()) useAsyncData<T, W>(
  1. Future<T> future(
    1. W watchValue
    ), {
  2. W watch()?,
})

Creates a reactive async operation that re-executes when watch function changes.

Unlike useFuture, this composable:

  1. Tracks changes in the watch function
  2. Automatically re-executes when watch function returns different value
  3. Passes watch value to the future function (if watch provided)
  4. Provides manual refresh function for triggering
  5. Returns detailed status and loading state

Type Parameters:

  • T: The type of data returned by the future
  • W: The type of value returned by the watch function (defaults to void)

Parameters:

  • future: The async function to execute. Receives watch value if watch function is provided
  • watch: Optional function that returns a value to watch. When the returned value changes, the future is automatically re-executed with the new value

Returns a tuple of:

  • status: Reactive AsyncValue with full state (idle/loading/data/error)
  • refresh: Function to manually trigger the async operation

Derive data / error / loading refs from status with computed when needed, or use useAsyncState instead if you don't need the raw AsyncValue for pattern matching.

Concurrency: if refresh is called (or the watched value changes) while a previous request is still in flight, a new request is started and the previous request's result is discarded (latest-wins).

Example with watch function:

@override
Widget Function(BuildContext) setup() {
  final userId = ref(1);

  final (status, refresh) = useAsyncData<User, int>(
    (id) => api.fetchUser(id), // Receives userId
    watch: () => userId.value, // Re-executes when userId changes
  );
  final data = computed(() => status.value.dataOrNull);
  final loading = computed(() => status.value.isLoading);

  return (context) => Column(
    children: [
      if (loading.value)
        const CircularProgressIndicator()
      else if (data.value != null)
        Text('User: ${data.value!.name}'),
      TextField(
        onChanged: (value) => userId.value = int.parse(value),
      ),
    ],
  );
}

Example without watch (executes once on mount):

@override
Widget Function(BuildContext) setup() {
  final (status, refresh) = useAsyncData<String, void>(
    (_) => fetchData(),
  );

  return (context) {
    return switch (status.value) {
      AsyncLoading() => const CircularProgressIndicator(),
      AsyncError(:final errorValue) => Text('Error: $errorValue'),
      AsyncData(:final value) => Text('Data: $value'),
      AsyncIdle() => ElevatedButton(
          onPressed: refresh,
          child: const Text('Load'),
        ),
    };
  };
}

Example with manual refresh:

@override
Widget Function(BuildContext) setup() {
  final (status, refresh) = useAsyncData<List<Item>, void>(
    (_) => api.fetchItems(),
  );
  final loading = computed(() => status.value.isLoading);

  return (context) => Column(
    children: [
      if (status.value case AsyncData(:final value))
        ...value.map((item) => ListTile(title: Text(item.name))),
      ElevatedButton(
        onPressed: loading.value ? null : refresh,
        child: const Text('Refresh'),
      ),
    ],
  );
}

Implementation

(ReadonlyRef<AsyncValue<T>> status, void Function() refresh) useAsyncData<T, W>(
  Future<T> Function(W watchValue) future, {
  W Function()? watch,
}) {
  assert(
    watch != null || null is W,
    'useAsyncData: when no `watch` function is provided, the watch type `W` '
    'must be `void` or nullable, e.g. useAsyncData<T, void>((_) => ...).',
  );

  final statusRef = ref<AsyncValue<T>>(const AsyncValue.idle());

  // Monotonically increasing request id. A newer request supersedes older
  // in-flight ones: stale completions are dropped instead of new requests.
  var requestId = 0;

  Future<void> refresh() async {
    final id = ++requestId;

    statusRef.value = const AsyncValue.loading();

    final watchValue = (watch != null ? watch() : null) as W;

    try {
      final result = await future(watchValue);
      if (id == requestId) {
        statusRef.value = AsyncValue.data(result);
      }
    } on Object catch (error, stackTrace) {
      if (id == requestId) {
        statusRef.value = AsyncValue.error(error, stackTrace);
      }
    }
  }

  // Watch the function and re-execute when it changes
  if (watch != null) {
    final watchFn = watch; // Capture to avoid shadowing
    fw.watch(watchFn, (newVal, oldVal) {
      // Only refresh if value actually changed
      if (newVal != oldVal) {
        unawaited(refresh());
      }
    });
  }

  // Execute once on mount for initial load
  onMounted(refresh);

  return (statusRef, refresh);
}