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)
  • loading: Reactive boolean indicating if operation is in progress
  • refresh: Function to manually trigger the async operation

Example with watch function:

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

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

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

Example without watch (executes once on mount):

@override
Widget Function(BuildContext) setup() {
  final (status, loading, 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, loading, refresh) = useAsyncData<List<Item>, void>(
    (_) => api.fetchItems(),
  );

  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,
}) {
  final statusRef = ref<AsyncValue<T>>(const AsyncValue.idle());

  Future<void> refresh() async {
    if (statusRef.value.isLoading) return; // Prevent concurrent executions

    statusRef.value = const AsyncValue.loading();

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

    await future(watchValue).then(
      (result) {
        statusRef.value = AsyncValue.data(result);
      },
      onError: (Object error, StackTrace stackTrace) {
        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
    // Use watch API to track value changes and trigger refresh
    fw.watch(watchFn, (newVal, oldVal) {
      // Only refresh if value actually changed
      // This prevents infinite loops when watch source is re-evaluated
      if (newVal != oldVal) {
        // Don't await here - refresh updates statusRef asynchronously
        unawaited(refresh());
      }
    });

    // Execute once on mount for initial load
    onMounted(refresh);
  } else {
    // Execute once on mount if no watch function provided
    onMounted(refresh);
  }

  return (statusRef, refresh);
}