riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Mention in a migration guide that `await ref.read(provider.future)` can cause an infinite await. => use listen

Open rrousselGit opened this issue 1 year ago • 6 comments

The solution is to instead do:

final sub = ref.listen(provider.future, (p, n){});

try {
  await sub.read();
} finally {
  sub.close();
}

This is inconvenient, but generally speaking mutations should cover most cases where we currently use ref.read.

rrousselGit avatar Sep 14 '24 23:09 rrousselGit

In what cases does it cause the infinite await?

mboyamike avatar Sep 19 '24 12:09 mboyamike

It's 3.0 stuff

rrousselGit avatar Sep 19 '24 14:09 rrousselGit

Is it possible for the infinite await issue to occur in 2.x versions as well?

I’m not completely sure if this is the root cause in my case, but I’ve noticed that when I have a Provider B that depends on Provider A (where Provider A runs invalidateSelf on a timer), Provider B sometimes gets stuck in a perpetual loading state until Provider A refreshes again. It seems to happen when the invalidation of Provider A coincides with the rebuilding of Provider B.

Could this be related to the issue you described?

rhinck avatar Sep 19 '24 19:09 rhinck

No this is unrelated

rrousselGit avatar Sep 20 '24 05:09 rrousselGit

Is this due to the retry feature #3623?

snapsl avatar Sep 22 '24 10:09 snapsl

Is this only happening in version 3? Because I can reproduce this in version 2.6.1. This happens when the provider's future that's being awaited has its dependency changed while computing.

I can fix this using the solution above (using ref.listenManual)

dartpad: https://dartpad.dev/?id=486e613330292488eef8f36e67168ee9

Code
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MaterialApp(home: Home())));
}

final dependencyProvider = FutureProvider(
  (ref) async {
    await Future.delayed(const Duration(milliseconds: 200));
    return Random().nextInt(100);
  },
);

final dataProvider = FutureProvider(
  (ref) async {
    ref.onDispose(() => print('dispose'));
    print('computing dependency...');
    final dependency = await ref.watch(dependencyProvider.future);
    print('computing data from dependency ($dependency)...');
    await Future.delayed(const Duration(seconds: 1));
    return dependency + 1;
  },
);

class Home extends ConsumerStatefulWidget {
  const Home({super.key});

  @override
  ConsumerState<Home> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<Home> {
  @override
  void initState() {
    super.initState();
    () async {
      // Wait for the dependency to finish computing
      await ref.read(dependencyProvider.future);

      () async {
        await Future.delayed(const Duration(milliseconds: 100));
        ref.invalidate(dependencyProvider);
      }();

      // Use listenManual to fix

      // final sub = ref.listenManual(
      //   dataProvider.future,
      //   (previous, next) {},
      // );
      //
      // try {
      //   final data = await sub.read();
      //   print(data);
      // } finally {
      //   sub.close();
      // }

      final data = await ref.read(dataProvider.future); // Infinite await
      print(data);
    }();
  }

  @override
  Widget build(BuildContext context) {
    ref.watch(dependencyProvider);
    return const Placeholder();
  }
}

kefasjw avatar Mar 24 '25 08:03 kefasjw

Our codebase utilizes the await container.read(provider.future) pattern in over 50 places, primarily to create/get objects during initialization and within background task handlers.

So I use the following extension with Riverpod 3.0 to keep it a single call:

extension ProviderContainerReadAsyncExt on ProviderContainer {
  Future<ValueT> readAsync<ValueT>(
    AsyncProviderListenable<ValueT> provider,
  ) async {
    final ProviderSubscription<Future<ValueT>> sub = listen(
      provider.future,
      (_, _) {},
    );

    try {
      return await sub.read();
    } finally {
      sub.close();
    }
  }
}

karelklic avatar Aug 23 '25 16:08 karelklic

Hi, as per above, I would also like to know when is this actually a concern. Our code base is calling await ref.read(myProvider.future) in several places and they have not gotten stuck yet. The recommendation above was to use ref.listen(), but this returns void instead of a subscription, perhaps it should be ref.listenManual()?

mash-g avatar Oct 24 '25 11:10 mash-g

Ref.listen or WidgetRef.listenManual

rrousselGit avatar Oct 24 '25 12:10 rrousselGit

Ok, thanks for confirming. Will riverpod_lint be updated to provide a warning in this case?

Edit: using below based on karelklic's answer, no issues found in some quick testing, simply replaced ref.read() with ref.readAsync() due to same interface, though in our case ref.read(provider.future) wasn't getting stuck anyway but better to follow the recommended approach.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/misc.dart';

extension RefExtensions on Ref {
  // docs: https://github.com/rrousselGit/riverpod/issues/3745
  StateT readAsync<StateT>(
    ProviderListenable<StateT> listenable,
  ) {
    final sub = listen(listenable, (_, _) {});
    try {
      return sub.read();
    } finally {
      sub.close();
    }
  }
}

extension WidgetRefExtensions on WidgetRef {
  // docs: https://github.com/rrousselGit/riverpod/issues/3745
  StateT readAsync<StateT>(
    ProviderListenable<StateT> listenable,
  ) {
    final sub = listenManual(listenable, (_, _) {});
    try {
      return sub.read();
    } finally {
      sub.close();
    }
  }
}

Perhaps it makes sense to update ref.read() to do this internally? Then users won't be affected and can avoid updating riverpod_lint.

mash-g avatar Oct 24 '25 12:10 mash-g

I suspect that won't work - you need to await within the try or else you're back to normal read()

TekExplorer avatar Oct 24 '25 21:10 TekExplorer

A crucial issue here is that Ref.read sometimes seems to work fine with FutureProvider and StreamProvider. When it doesn't work, it creates a difficult-to-diagnose bug: An awaited future somewhere deep inside your provider never finishes, and nothing in the logs or the debugger tells you why. Or one of your streams never yields a value, even when it is guaranteed to do so.

This makes Ref.read dangerous to use. It should throw an exception when called with an async provider.

karelklic avatar Oct 25 '25 09:10 karelklic

The main issue is that this:

await ref.read(provider.notifier).doSomethingAsync();

is equally unsafe.

I'm heavily considering deprecating ref.read altogether, in favour of the approach taken by mutations

rrousselGit avatar Oct 25 '25 14:10 rrousselGit

@rrousselGit am I correct by using it the following way?:

Future<void> onPushNotificationsChanged({required bool enabled}) async {
      final scheduledSubscription = ref.container.listen(hasScheduledNotificationsProvider.future, (previous, next) {});

      try {
        final hasScheduledNotifications = await scheduledSubscription.read();
      } finally {
        scheduledSubscription.close();
      }
}

or should it be:

final sub = ref.listenManual(hasScheduledNotificationsProvider.future, (previous, next) {});

mpoimer avatar Nov 03 '25 10:11 mpoimer

I really liked the option to read a Stream/Future provider and basically skip the first loading state. I mean, this is more or less what .future was doing in v2 and we were using it a lot. Now we are migrating to a safer approach after reading this issue, but it would be amazing to have this back somehow. This would be useful especially in situations where we need to use a provider in a background thread or in some place that is not UI so we don't want updates, but we just want the first real value the provider emits.

imtoori avatar Nov 11 '25 10:11 imtoori

Has anyone got a version of readAsync for refresh?

josh-burton avatar Nov 12 '25 18:11 josh-burton