ref.watch vs ref.read vs ref.listen
This is Part 5 of Mastering Riverpod: Foundation — the finale before the question bank. You've used ref.watch already; now we cover all three ways to read a provider and, more importantly, when to use each. Picking the wrong one is the single most common Riverpod bug — stale UIs, infinite rebuild loops, or side effects firing at the wrong time all trace back to misusing ref. Let's make the rules second nature.
The ref (from a ConsumerWidget's build, a Consumer, or inside a provider) has three reading methods: watch, read, and listen.
The 10-second summary
| Method | What it does | Use it in… | Rebuilds the widget? |
| --- | --- | --- | --- |
| ref.watch | read + subscribe; reacts to changes | build / provider create functions | yes, on change |
| ref.read | read once, no subscription | callbacks (onPressed, initState) | no |
| ref.listen | run a side-effect callback on change | build (sets up the listener) | no (runs your callback) |
The mental model: watch is for displaying, read is for doing, listen is for reacting. Keep those three verbs in mind and you'll almost always pick right.
ref.watch — for building UI (and deriving providers)
ref.watch reads a provider's value and subscribes to it. When the value changes, the watcher (a widget or another provider) is rebuilt/recomputed. This is the default — use it whenever your UI should reflect the current value:
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // subscribe
return Text('$count'); // rebuilds when count changes
}
And inside a provider, to depend on another (Part 4):
final doubledProvider = Provider<int>((ref) {
final count = ref.watch(counterProvider); // recompute when counter changes
return count * 2;
});
The golden rule: in
build(and in provider create functions), useref.watch. It keeps the UI/derived state in sync with the source of truth. If you find yourself wanting the UI to update, you wantwatch.
Don't watch in a callback
ref.watch is for the declarative part of your code (build). Calling it inside an onPressed would try to set up a subscription every tap — wrong tool, and riverpod_lint will warn you. Callbacks use read (next).
ref.read — for one-off actions in callbacks
ref.read reads the current value once and does not subscribe. Use it inside event handlers and callbacks, where you just need the value (or the notifier) at the moment of the action and you do not want to rebuild:
ElevatedButton(
onPressed: () {
// Read the current value once to perform an action.
final current = ref.read(counterProvider);
print('counter is $current');
},
child: const Text('Print'),
)
The most common use of ref.read is to call a method on a notifier (mutating state — you'll meet notifiers in Series 2):
onPressed: () {
// Call a method to change state — in a callback, so use read.
ref.read(counterProvider.notifier).increment();
}
Why
readhere, notwatch? A button'sonPressedruns in response to a tap, not duringbuild. You don't want the button to "rebuild" — you want to act.ref.readgives the current value/notifier without creating a subscription.
The classic bug: ref.read in build
Here's the mistake everyone makes once. Using ref.read in build to "just get the value":
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.read(counterProvider); // ❌ reads once, NO subscription
return Text('$count'); // ← never updates when counter changes — STALE UI
}
Because read doesn't subscribe, the widget never rebuilds when counterProvider changes — the number on screen is frozen. In build, use watch. Reach for read only in callbacks/lifecycle code where you explicitly don't want a subscription.
Memory aid:
readis for reading and reacting now (in a callback);watchis for reflecting continuously (in build). If the result is rendered, it must bewatch.
ref.listen — for side effects (not UI)
Sometimes a provider change should trigger an action, not a rebuild — show a snackbar, pop a dialog, navigate, log analytics. That's ref.listen. You set it up in build, and it calls your callback when the value changes, giving you the previous and next values:
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<void>>(submitProvider, (previous, next) {
// Runs on change — a side effect, not a rebuild.
if (next is AsyncError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed: ${next.error}')),
);
}
});
return const SubmitForm();
}
Key points about ref.listen:
- You register it in
build(so it's tied to the widget's lifecycle), but it does not rebuild the widget — it runs your callback for side effects. - The callback receives
(previous, next), so you can react to transitions (e.g. "went from loading to error"). - It's the right place for things you should never do in
build: navigation, dialogs, snackbars (doing those inbuildis a bug, sincebuildcan run many times — Flutter Part 4).
Riverpod 3.0 note:
ref.listensupports pause/resume, and Riverpod auto-pauses listeners when the widget is off-screen (viaTickerMode). You generally don't manage this — just know listeners won't fire side effects for invisible widgets.
There's also ref.listenManual for listening outside build (e.g. in initState), which returns a subscription you must close yourself — but ref.listen in build covers the common case.
Putting the three together
A single screen often uses all three, each for its job:
class CartScreen extends ConsumerWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// WATCH — display the live cart total.
final total = ref.watch(cartTotalProvider);
// LISTEN — react to checkout result with a side effect.
ref.listen(checkoutProvider, (prev, next) {
if (next is AsyncData) Navigator.of(context).pushNamed('/success');
});
return Scaffold(
body: Text('Total: \$$total'),
floatingActionButton: FloatingActionButton(
// READ — perform an action on tap (no subscription/rebuild).
onPressed: () => ref.read(cartProvider.notifier).checkout(),
child: const Icon(Icons.payment),
),
);
}
}
watch to show the total, read to trigger checkout, listen to react to its result. That's the canonical division of labor.
Decision guide
Do you need the value rendered in the UI (or to derive another provider)?
└── Yes → ref.watch (in build / provider create)
Are you in a callback (onPressed, onTap, initState) and just need to act?
└── Yes → ref.read (read value or call a notifier method)
Do you want to run a side effect (snackbar, navigate, dialog, log) on change?
└── Yes → ref.listen (set up in build; gives previous/next)
The two rules that prevent almost every ref bug:
- In
build, never useref.readfor something you display — it won't update. Usewatch. - In callbacks, never use
ref.watch— useread(or call a notifier method viaread).
Practice Challenges
Challenge 1 — Display a value. Show userNameProvider in a ConsumerWidget so it updates on change.
Show solution
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(userNameProvider);
return Text(name);
}
watch subscribes, so the Text updates when the name changes.
Challenge 2 — Act on tap. On a button press, call increment() on counterProvider's notifier.
Show solution
onPressed: () => ref.read(counterProvider.notifier).increment(),
In a callback, use read to get the notifier and call its method — no subscription needed.
Challenge 3 — Fix the stale UI. Why doesn't this update, and how do you fix it?
Widget build(BuildContext context, WidgetRef ref) {
final n = ref.read(counterProvider);
return Text('$n');
}
Show solution
ref.read reads once with no subscription, so the widget never rebuilds when counterProvider changes — the text is frozen. Use ref.watch in build:
final n = ref.watch(counterProvider);
Challenge 4 — Side effect on change. Show a snackbar whenever errorProvider becomes non-null.
Show solution
ref.listen<String?>(errorProvider, (prev, next) {
if (next != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(next)));
}
});
ref.listen runs the side effect on change (with previous/next) — never do snackbars/navigation in build directly.
Challenge 5 — Choose for each. watch / read / listen for: (a) showing a counter, (b) navigating when login succeeds, (c) submitting a form on tap.
Show solution
(a) watch (display, rebuild on change). (b) listen (side effect — navigation — on a state transition). (c) read (call a method in a callback, no rebuild).
Questions to test yourself
Q1 (basic). What's the difference between ref.watch and ref.read?
Show answer
ref.watch reads and subscribes — the watcher rebuilds when the value changes (use in build). ref.read reads once with no subscription — no rebuild (use in callbacks). Watch reflects continuously; read acts now.
Q2 (basic). Which method do you use to display a provider's value in build?
Show answer
ref.watch — so the widget rebuilds and stays in sync when the value changes.
Q3 (intermediate). Why does using ref.read in build to show a value cause a bug?
Show answer
ref.read doesn't subscribe, so the widget never rebuilds when the provider changes — the displayed value goes stale/frozen. Displayed values must use ref.watch.
Q4 (intermediate). What is ref.listen for, and where do you set it up?
Show answer
For side effects that should run when a provider changes — navigation, snackbars, dialogs, logging — not rebuilding UI. You set it up in build; it calls your callback with (previous, next) on change (and doesn't rebuild the widget).
Q5 (intermediate). Why use ref.read (not watch) inside an onPressed?
Show answer
A callback runs in response to an event, not during build; you want to act, not subscribe. ref.watch in a callback would attempt to create a subscription on every invocation (wrong, and lint-flagged). ref.read gives the current value or the notifier to call a method.
Q6 (advanced). Why must side effects like navigation go in ref.listen rather than directly in build?
Show answer
build can run many times and at unpredictable moments (Flutter Part 4), so performing navigation/snackbars/dialogs there would fire them repeatedly and at the wrong times. ref.listen runs its callback only on an actual state change (with previous/next so you can detect specific transitions), making it the correct, controlled place for one-shot side effects.
Wrapping up
The three ref methods, each with a clear job:
ref.watch— read + subscribe; use inbuildand provider create functions to display/derive reactive state. (reflect)ref.read— read once, no subscription; use in callbacks/lifecycle to grab a value or call a notifier method. (act)ref.listen— run a side-effect callback on change (snackbar, navigate, log), set up inbuildwith(previous, next). (react)- Two rules: display →
watch, callback →read; neverreada displayed value orwatchin a callback.
That completes the Foundation concepts — you can set up Riverpod, understand where state lives, declare and read providers, and choose the right ref method. Time to prove it. Part 6 is the 100-question Riverpod Foundation mastery bank with coding mini-exercises.