ref.invalidate vs ref.refresh
This is Part 5 of Mastering Riverpod: Core Concepts. Providers cache their value and recompute automatically when dependencies change. But sometimes you need to force a recompute on demand — a retry button after an error, pull-to-refresh, or "reload after the user saved something." Riverpod gives you two methods for this: ref.invalidate and ref.refresh. They look interchangeable but differ in one important way. Let's nail it down.
The shared idea: discard the cached value
Both methods tell Riverpod: "throw away this provider's cached state and recompute it." For a FutureProvider/AsyncNotifier, that means re-running the fetch. For a Provider, it re-runs the create function. The difference is purely about when the recompute happens and whether you get the result back.
ref.invalidate — mark for recompute (lazy)
ref.invalidate(provider) marks the provider's state as stale. The recompute happens lazily:
- If the provider is currently being watched, it recomputes (so watchers rebuild).
- If nothing is watching it, it's simply disposed and will recompute the next time it's read.
ref.invalidate(userProvider); // returns void — fire and forget
invalidate returns void — you're not asking for the new value, you're just saying "this is stale, redo it." It's the right choice for "something changed, refresh the relevant providers" where you don't need the result inline.
A handy variant:
ref.invalidateSelf()— called inside a provider/notifier to recompute itself (e.g. arefresh()method on anAsyncNotifierthat re-runs its ownbuild()).
ref.refresh — invalidate and read (eager, returns value)
ref.refresh(provider) does exactly what invalidate does plus immediately re-reads the provider, returning the new value:
final newUser = ref.refresh(userProvider); // recompute AND get the result
// refresh is literally equivalent to:
ref.invalidate(userProvider);
final newUser2 = ref.read(userProvider);
So refresh = invalidate + read. Use it when you need the freshly-computed value right there — most commonly to await a re-fetch:
// Pull-to-refresh: re-run the fetch and await its completion.
onRefresh: () => ref.refresh(userProvider.future),
ref.refresh(userProvider.future) re-triggers the fetch and returns the Future you can await (so the refresh indicator spins until it completes).
The difference, distilled
| | ref.invalidate | ref.refresh |
| --- | --- | --- |
| Recompute? | yes (lazily) | yes (immediately) |
| Returns | void | the new value |
| Mental model | "mark stale" | "mark stale and read now" |
| Use for | "something changed, redo it" (no result needed) | "redo it and give me the result" (await/use it) |
The one-liner:
refreshisinvalidatefollowed byread. If you need the new value (especially toawait), userefresh. If you just want to trigger a recompute, useinvalidate. When in doubt for pull-to-refresh,ref.refresh(provider.future)is the idiom.
Modern guidance: the Riverpod team generally recommends
invalidatefor "just refresh it" cases (it avoids accidentally reading a provider you don't need), andrefreshspecifically when you want the returned value. Both are fine; pick by whether you use the result.
Invalidating a family
For a family, you can invalidate one specific argument or, by passing the family itself, all of its instances:
ref.invalidate(userProvider('123')); // just user 123 recomputes
ref.invalidate(userProvider); // ALL cached user-by-id instances recompute
The second form is great after a bulk change ("I updated everyone, refresh every cached user"). Pair this with the autoDispose recommendation for families and you have clean, controllable per-argument caching.
Common patterns
Retry after an error
ref.watch(dataProvider).when(
error: (e, st) => Column(children: [
Text('Failed: $e'),
ElevatedButton(
onPressed: () => ref.invalidate(dataProvider), // retry
child: const Text('Retry'),
),
]),
loading: () => const Spinner(),
data: (d) => DataView(d),
);
Pull-to-refresh (await the result)
RefreshIndicator(
onRefresh: () => ref.refresh(postsProvider.future), // await the re-fetch
child: PostList(/* ... */),
);
Reload after a mutation
Future<void> save() async {
await ref.read(repoProvider).update(item);
ref.invalidate(itemListProvider); // list is now stale → recompute
}
(Often an AsyncNotifier mutation re-fetches internally instead — Provider Types Part 5 — but invalidate is perfect when the changed data lives in a different provider.)
A subtlety: invalidate doesn't always recompute immediately
Because invalidate is lazy, if nothing is watching the provider when you call it, it won't recompute until something reads it again. This is usually what you want (don't do work for data no one needs), but it can surprise you in tests or background logic — there, use refresh (or read after invalidate) to force the recompute now. The mental model holds: invalidate marks stale; refresh marks stale and reads.
Practice Challenges
Challenge 1 — Pick one. You want to retry a failed fetch from an error button and don't need the value inline. Which method?
Show solution
ref.invalidate(provider) — you just want to trigger a recompute; the watching widget rebuilds with the new state. No need for the returned value.
Challenge 2 — Await a refresh. Write a pull-to-refresh that awaits the re-fetch of feedProvider.
Show solution
onRefresh: () => ref.refresh(feedProvider.future),
refresh re-runs the fetch and returns the Future to await (so the indicator spins until done).
Challenge 3 — Equivalence. Express ref.refresh(p) in terms of invalidate and read.
Show solution
ref.invalidate(p);
final value = ref.read(p);
refresh = invalidate + read, returning the new value.
Challenge 4 — Family. Invalidate just user 42, then all users.
Show solution
ref.invalidate(userProvider(42)); // one instance
ref.invalidate(userProvider); // all instances
Challenge 5 — Self-refresh. How does an AsyncNotifier re-run its own build()?
Show solution
ref.invalidateSelf() inside the notifier — it marks the notifier stale and re-runs build(). Often wrapped in a refresh() method exposed to the UI.
Questions to test yourself
Q1 (basic). What do both invalidate and refresh do?
Show answer
Both discard a provider's cached state and cause it to recompute (e.g. re-run the fetch for an async provider). The difference is timing and return value.
Q2 (basic). What does ref.invalidate return, and what does ref.refresh return?
Show answer
invalidate returns void (just marks stale). refresh returns the newly-computed value (it invalidates and immediately reads).
Q3 (intermediate). Express refresh in terms of invalidate.
Show answer
ref.refresh(p) is equivalent to ref.invalidate(p) followed by ref.read(p) — invalidate, then read the new value.
Q4 (intermediate). Why is invalidate described as "lazy"?
Show answer
Because it only marks the provider stale. If the provider is currently watched it recomputes (watchers rebuild); if nothing watches it, it's disposed and recomputes only on the next read. It doesn't force an immediate recompute on its own.
Q5 (intermediate). For pull-to-refresh, what's the idiomatic call and why?
Show answer
ref.refresh(provider.future) — it re-runs the fetch and returns the Future, which the RefreshIndicator awaits so the spinner stays until the new data arrives. (invalidate wouldn't give you a future to await.)
Q6 (advanced). When invalidating a family, what's the difference between ref.invalidate(p(arg)) and ref.invalidate(p)?
Show answer
ref.invalidate(p(arg)) invalidates only the one instance for that argument. ref.invalidate(p) (passing the family itself) invalidates all cached instances of the family — useful after a bulk change that affects every argument's data.
Wrapping up
invalidate and refresh force recomputation:
- Both discard cached state and recompute; the difference is timing/return.
ref.invalidatemarks stale (lazy, returnsvoid) — "something changed, redo it."ref.refresh=invalidate+read(eager, returns the new value) — use when you need the result, especiallyref.refresh(p.future)for pull-to-refresh.invalidateSelf()recomputes a provider from inside itself; family invalidation can target one argument or all instances.- Default to
invalidatefor "just refresh"; userefreshwhen you use the returned value.
We keep saying providers recompute when their dependencies change. But how exactly does one provider depend on another, and how does a change ripple through the graph? Part 6 examines provider dependencies — how providers watch other providers.