skip to Main Content

I’m using Riverpod to listen to changes to a selected item from my store:

  1. I have a repository PlacesStore which has a Stream<PlaceDetails?> watchPlace(PlaceId placeId) method.
  2. I have wrapped this in a family provider like so:
    @riverpod
    Stream<PlaceDetails?> place(PlaceRef ref, InternalPlaceId placeId) {
        final store = ref.watch(placesStoreProvider);
        return store.watchPlace(placeId);
    }
    
  3. I have a separate StateProvider, selectedPlaceIdProvider, which is updated with a place ID of a place the user selects on the map.
  4. I have another provider, selectedPlaceProvider, which combines both like this:
    @riverpod
    Stream<PlaceDetails?> selectedPlace(SelectedPlaceRef ref) {
        final placeId = ref.watch(selectedPlaceIdProvider);
        if (placeId != null) {
            return ref.watch(placeProvider(placeId).stream);
        }
    
        return const Stream.empty();
     }
    

I was doing it like this because I have a number of widgets which want to know what place the user has selected, so I thought it made sense to pull that into a provider which contains the currently selected place details (while still maintaining the family provider so adhoc places can be looked up).

However, when using ref.watch(placeProvider(placeId).stream) I am warned that .stream is deprecated. I don’t quite follow what I am meant to replace it with.

Is the above pattern valid? And if so, what do I replace .stream with so I can still listen to any changes to my currently selected place.

2

Answers


  1. You really never need a stream from a provider. If you want a view or a provider to depend on other provider changes, simply subscribe with ref.watch or ref.listen.

    Streams have no place in state management. Streams are useful for places where having every single intermediate state is important to record, like a log or a bank transaction ledger. But in state management, having the latest value is all that’s important. You merely need your view or your provider to consume the other provider’s latest value, and you’re done.

    And .stream was removed, because it made some other parts of the code difficult. If you actually need a stream, you can listen to a provider, and generate your own stream using your rules.

    Login or Signup to reply.
  2. The .stream modifier has been deprecated because it almost always was badly used, leading to complex hard-to-spot bugs.

    The very snippet you gave in this question faces the same issue. The snippet you gave has a bug:

    @riverpod
    Stream<PlaceDetails?> selectedPlace(SelectedPlaceRef ref) {
        final placeId = ref.watch(selectedPlaceIdProvider);
        if (placeId != null) {
            return ref.watch(placeProvider(placeId).stream);
        }
    
        return const Stream.empty();
    }
    

    In this snippet, the usage of .stream introduces a problem where now the behavior of selectedPlace depends on the order in which your providers are read.

    Say you did:

    void main() async {
      final container = ProviderContainer();
      final place = await container.read(placeProvider(123).future);
    
      container.read(selectedPlaceIdProvider.notifier).state = 123;
      container.listen(selectedPlaceProvider, (_, value) {
        print('Place $value');
      });
    }
    

    Then in this scenario, you’ll find that the print is in fact never reached, and selectedPlace never emits anything.
    But if you were to comment out the final place = await container.read(placeProvider(123).future); line, then your code would "work".

    That effectively means using .stream introduced a race condition. It’s very hard to spot unless you’re familiar with the issue, and can cause significant maintainability problems.

    On the flip side of things, having .stream doesn’t add much value to Riverpod. You can already do everything without .stream.

    The Changelog that introduced the deprecation gives alternate syntaxes to your usage https://github.com/rrousselGit/riverpod/blob/master/packages/riverpod/CHANGELOG.md#230

    In your case, you could refactor selectedPlaceProvider to:

    @riverpod
    Future<PlaceDetails?> selectedPlace(SelectedPlaceRef ref) {
        final placeId = ref.watch(selectedPlaceIdProvider);
        if (placeId == null) return null;
    
        return ref.watch(placeProvider(placeId).future);
    }
    

    This snippet behave like you would expect the .stream variant to behave. In that scenario, selectedPlace will update whenever either placeProvider or selectedPlaceIdProvider update.
    But at the same time, this doesn’t involve the race-condition problem discussed above. That main wrote previously would now work as expected.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search