← Back to blog
Mastering Riverpod: Core Concepts · Part 6 of 8
August 21, 20268 min read

Provider Dependencies — How Providers Watch Other Providers

RiverpodFlutterDart

Provider Dependencies

This is Part 6 of Mastering Riverpod: Core Concepts. We've said many times that providers form a reactive graph — one provider depends on another, and changes ripple through. Now we make that precise: how a provider depends on another, what propagates when something changes, and the pitfalls (stale reads, circular dependencies). This is the idea that lets you decompose an app into small, composable pieces of state.


A provider's ref can read other providers

Every provider's create function (and every notifier) receives a ref (Foundation Part 5). Inside a provider, that ref reads other providers — and how you read determines the dependency relationship. The same three methods from the UI apply, with the same meanings:

| Inside a provider | Effect | Use for | | --- | --- | --- | | ref.watch(other) | depend on other; recompute this provider when it changes | deriving state from dependencies | | ref.read(other) | read other once, no dependency | one-off reads in methods/callbacks | | ref.listen(other, cb) | run a side effect when other changes | reacting without recomputing |

This is the same watch/read/listen you learned for widgets — Riverpod is consistent: providers and widgets read state the same way.


ref.watch: the dependency that builds the graph

ref.watch inside a provider is what creates an edge in the dependency graph. The provider becomes a dependent of what it watches, and recomputes whenever that dependency emits a new value:

final firstNameProvider = Provider<String>((ref) => 'Vivek');
final lastNameProvider  = Provider<String>((ref) => 'Kumar');

// fullNameProvider DEPENDS ON the two above.
final fullNameProvider = Provider<String>((ref) {
  final first = ref.watch(firstNameProvider); // edge: fullName ← firstName
  final last = ref.watch(lastNameProvider);   // edge: fullName ← lastName
  return '$first $last';
});

The graph:

firstNameProvider ──┐
                    ├──► fullNameProvider ──► (widgets watching fullName)
lastNameProvider  ──┘

Change firstNameProviderfullNameProvider recomputes → every widget watching fullNameProvider rebuilds. You declared what depends on what, and Riverpod propagates changes automatically — the spreadsheet model made concrete.


Propagation: how a change ripples

When a provider's value changes, Riverpod walks its dependents and recomputes them, then their dependents, and so on — but it's smart about stopping:

  1. The changed provider notifies its direct dependents.
  2. Each dependent recomputes. If its new value equals the old (==), propagation stops there — Riverpod 3.0 filters updates by equality (Provider Types Part 4), so unchanged results don't needlessly rebuild downstream.
  3. Otherwise the new value propagates to its dependents, recursively, down to the watching widgets.

This equality-based filtering is why deriving lots of small providers is cheap: a change only rebuilds the parts whose values actually changed. (You can customize the comparison via updateShouldNotify on a Notifier.)


ref.read inside a provider: use sparingly

ref.read(other) reads a value without creating a dependency — so this provider will not recompute when other changes. That's almost always wrong in a provider's build/create logic (you'd get a stale value), but right inside a method that performs an action:

class CartController extends Notifier<Cart> {
  @override
  Cart build() {
    final user = ref.watch(userProvider); // ✅ depend: rebuild if user changes
    return Cart.forUser(user);
  }

  void checkout() {
    // ✅ one-off action: read the repo without depending on it
    ref.read(checkoutRepositoryProvider).submit(state);
  }
}

The rule (same as the UI): in build/create logic, use ref.watch (you want to react to dependency changes). In methods/actions, use ref.read (one-off, no subscription). Using ref.read in build logic gives a stale value that never updates — the classic dependency bug.


ref.listen inside a provider: react without recomputing

Occasionally a provider should do something when a dependency changes without recomputing its own value — e.g. log out the cart when auth changes, or reset a timer. That's ref.listen inside the provider:

final sessionProvider = Provider<Session>((ref) {
  ref.listen(authStateProvider, (previous, next) {
    if (next == null) {
      // side effect on auth change — doesn't recompute `sessionProvider` itself
      clearLocalSession();
    }
  });
  return Session.current();
});

Same semantics as the UI's ref.listen (Foundation Part 5): a side-effect callback with (previous, next), no rebuild of the watcher.


Async dependencies

A provider can depend on an async provider. Watch its AsyncValue to react to state, or await its .future to chain (Provider Types Part 2):

final greetingProvider = FutureProvider<String>((ref) async {
  final user = await ref.watch(userProvider.future); // await the dependency
  return 'Hello, ${user.name}';
});

If userProvider re-fetches, greetingProvider re-runs too — the dependency edge works the same for async, just with AsyncValue/.future.


Pitfall: circular dependencies

If provider A watches B and B watches A, you've created a cycle — Riverpod can't compute either (each needs the other first) and throws. This usually signals a design problem: two pieces of state that are really one, or a dependency pointing the wrong way.

// ❌ A ← B and B ← A — circular, throws at runtime.
final aProvider = Provider<int>((ref) => ref.watch(bProvider) + 1);
final bProvider = Provider<int>((ref) => ref.watch(aProvider) + 1);

Fixes: merge the two into one provider, extract the shared input into a third provider both depend on, or break the cycle with a one-directional ref.read in an action (not build). The dependency graph must be a DAG (no cycles).


Designing with the graph

The payoff of understanding dependencies is decomposition: instead of one giant state object, build many small providers, each watching only what it needs. Benefits:

  • Granular rebuilds — only the providers (and widgets) downstream of a change recompute, and equality-filtering trims even those.
  • Testability — each provider is a small unit; override its dependencies with fakes (Foundation Part 3).
  • Clarity — the graph is your architecture; reading the ref.watches tells you exactly how data flows.

The whole Riverpod foundation was building to this: compose your app as a reactive graph of small providers.


Practice Challenges

Challenge 1 — Build an edge. Write taxProvider that's 8% of subtotalProvider.

Show solution
final taxProvider = Provider<double>((ref) => ref.watch(subtotalProvider) * 0.08);

ref.watch makes taxProvider recompute when the subtotal changes.

Challenge 2 — watch vs read. In a Notifier, where do you use watch and where read?

Show solution

ref.watch in build() (to depend on and react to other providers); ref.read in action methods (one-off reads, e.g. grabbing a repository to perform a write). Watching in build, reading in actions.

Challenge 3 — Stale bug. A derived provider uses ref.read on its source and never updates. Fix it.

Show solution

Change the ref.read(source) in its build/create logic to ref.watch(source)read creates no dependency, so the provider never recomputes when the source changes (stale). watch establishes the reactive edge.

Challenge 4 — Side effect. Log out when authProvider becomes null, from inside a provider, without recomputing it.

Show solution
ref.listen(authProvider, (prev, next) {
  if (next == null) logOut();
});

ref.listen runs a side effect on change without rebuilding the watcher.

Challenge 5 — Spot the cycle. Why does A = watch(B)+1; B = watch(A)+1; throw?

Show solution

It's a circular dependency: computing A needs B and computing B needs A, so neither can be initialized — Riverpod throws. Break the cycle (merge them, extract a shared third provider, or use one-directional read in an action). The graph must be acyclic.


Questions to test yourself

Q1 (basic). How does one provider depend on another?

Show answer

By calling ref.watch(otherProvider) inside its create function/build(). This creates a dependency edge: the provider recomputes whenever other changes.

Q2 (basic). What's the difference between ref.watch and ref.read inside a provider?

Show answer

ref.watch creates a dependency — the provider recomputes when the watched provider changes. ref.read reads once with no dependency — no recompute on change. Use watch in build logic, read in action methods.

Q3 (intermediate). When a provider's value changes, what stops the rebuild from propagating endlessly?

Show answer

Equality filtering: each dependent recomputes, and if its new value == the old, propagation stops there (Riverpod 3.0 filters updates by ==). Only dependents whose values actually change continue to propagate downstream. (updateShouldNotify can customize this.)

Q4 (intermediate). Why is using ref.read in a provider's build logic a bug?

Show answer

ref.read creates no dependency, so the provider won't recompute when that source changes — its derived value goes stale and never updates. Build/create logic should use ref.watch to stay reactive.

Q5 (intermediate). How does a provider react to a dependency change without recomputing its own value?

Show answer

With ref.listen(other, (prev, next) {...}) inside the provider — it runs a side-effect callback on change (e.g. clearing a session on logout) without rebuilding the watcher, just like ref.listen in widgets.

Q6 (advanced). What is a circular dependency, why does it fail, and how do you fix it?

Show answer

It's when providers watch each other (A ← B and B ← A), forming a cycle. It fails because computing each requires the other's value first, so neither can initialize — Riverpod throws. The dependency graph must be a DAG. Fix by merging the two into one provider, extracting their shared input into a third provider both depend on, or breaking the cycle with a one-directional ref.read inside an action (not build).


Wrapping up

Provider dependencies are the reactive graph:

  • A provider depends on another via ref.watch (creates an edge; recomputes on change); ref.read is a one-off no-dependency read (for actions); ref.listen runs side effects without recomputing.
  • Changes propagate to dependents, with == equality filtering stopping propagation where values don't actually change.
  • watch in build logic, read in action methodsread in build gives stale values (the classic bug).
  • Async dependencies work the same (ref.watch(p.future) to chain).
  • Avoid circular dependencies — the graph must be acyclic.

One last lifecycle hook completes the picture: when a provider is disposed (by autoDispose, invalidation, or scope teardown), how do you clean up its resources — timers, controllers, subscriptions? Part 7, the Core Concepts finale, covers ref.onDispose — cleanup logic in providers.