Skip to main content

Combining Provider States

Make sure to read about Providers first.
In this guide, we will learn about combining provider states.

Combining provider states

We've previously seen how to create a simple provider. But the reality is, in many situations a provider will want to read the state of another provider.

To do that, we can use the ref object passed to the callback of our provider, and use its watch method.

As an example, consider the following provider:

final cityProvider = Provider((ref) => 'London');

We can now create another provider that will consume our cityProvider:

final weatherProvider = FutureProvider((ref) async {
// We use `ref.watch` to listen to another provider, and we pass it the provider
// that we want to consume. Here: cityProvider
final city = ref.watch(cityProvider);

// We can then use the result to do something based on the value of `cityProvider`.
return fetchWeather(city: city);
});

That's it. We've created a provider that depends on another provider.

FAQ

What if the value being listened to changes over time?

Depending on the provider that you are listening to, the value obtained may change over time. For example, you may be listening to a StateNotifierProvider, or the provider being listened to may have been forced to refresh through the use of ProviderContainer.refresh/ref.refresh.

When using watch, Riverpod is able to detect that the value being listened to changed and will automatically re-execute the provider's creation callback when needed.

This can be useful for computed states. For example, consider a StateNotifierProvider that exposes a todo-list:

class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

A common use-case would be to have the UI filter the list of todos to show only the completed/uncompleted todos.

An easy way to implement such a scenario would be to:

  • create a StateProvider, which exposes the currently selected filter method:

    enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • make a separate provider which combines the filter method and the todo-list to expose the filtered todo-list:

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider);

    switch (filter) {
    case Filter.none:
    return todos;
    case Filter.completed:
    return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
    return todos.where((todo) => !todo.completed).toList();
    }
    });

Then, our UI can listen to filteredTodoListProvider to listen to the filtered todo-list.
Using such an approach, the UI will automatically update when either the filter or the todo-list changes.

To see this approach in action, you can look at the source code of the Todo List example.

info

This behavior is not specific to Provider, and works with all providers.

For example, you could combine watch with FutureProvider to implement a search feature that supports live-configuration changes:

// The current search filter
final searchProvider = StateProvider((ref) => '');

/// Configurations which can change over time
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');

return response.data.map((json) => Character.fromJson(json)).toList();
});

This code will fetch a list of characters from the service, and automatically re-fetch the list whenever the configurations change or when the search query changes.

Can I read a provider without listening to it?

Sometimes, we want to read the content of a provider, but without re-creating the value exposed when the value obtained changes.

An example would be a Repository, which reads from another provider the user token for authentication.
We could use watch and create a new Repository whenever the user token changes, but there is little to no use in doing that.

In this situation, we can use read, which is similar to watch, but will not cause the provider to recreate the value it exposes when the value obtained changes.

In that case, a common practice is to pass ref.read to the object created. The object created will then be able to read providers whenever it wants.

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
Repository(this.read);

/// The `ref.read` function
final Reader read;

Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);

final response = await dio.get('/path', queryParameters: {
'token': token,
});

return Catalog.fromJson(response.data);
}
}
note

You could also pass the ref instead of ref.read to your object:

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
Repository(this.ref);

final Ref ref;
}

The only difference that passing ref.read brings is that it is slightly less verbose and ensures that our object never uses ref.watch.

DON'T call read inside the body of a provider
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});

If you used read as an attempt to avoid unwanted rebuilds of your object, refer to My provider updates too often, what can I do?

How to test an object that receives read as a parameter of its constructor?

If you are using the pattern described in Can I read a provider without listening to it?, you may be wondering how to write tests for your object.

In this scenario, consider testing the provider directly instead of the raw object. You can do so by using the ProviderContainer class:

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
final container = ProviderContainer();
addTearOff(container.dispose);

Repository repository = container.read(repositoryProvider);

await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});

My provider updates too often, what can I do?

If your object is re-created too often your provider is likely listening to objects that it doesn't care about.

For example, you may be listening to a Configuration object, but only use the host property.
By listening to the entire Configuration object, if a property other than host changes, this still causes your provider to be re-evaluated – which may be undesired.

The solution to this problem is to create a separate provider that exposes only what you need in Configuration (so host):

AVOID listening to the entire object:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// Will cause productsProvider to re-fetch the products if anything in the
// configurations changes
final configs = await ref.watch(configProvider.future);

return dio.get('${configs.host}/products');
});

PREFER using select when you only need a single property of an object:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// Listens only to the host. If something else in the configurations
// changes, this will not pointlessly re-evaluate our provider.
final host = await ref.watch(configProvider.selectAsync((config) => config.host));

return dio.get('$host/products');
});

This will only rebuild the productsProvider when the host changes.