← Back to blog
Mastering Riverpod: Core Concepts · Part 5 of 8
August 20, 20267 min read

ref.invalidate vs ref.refresh — Forcing Provider Recomputation

RiverpodFlutterDart

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. a refresh() method on an AsyncNotifier that re-runs its own build()).


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: refresh is invalidate followed by read. If you need the new value (especially to await), use refresh. If you just want to trigger a recompute, use invalidate. When in doubt for pull-to-refresh, ref.refresh(provider.future) is the idiom.

Modern guidance: the Riverpod team generally recommends invalidate for "just refresh it" cases (it avoids accidentally reading a provider you don't need), and refresh specifically 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.invalidate marks stale (lazy, returns void) — "something changed, redo it."
  • ref.refresh = invalidate + read (eager, returns the new value) — use when you need the result, especially ref.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 invalidate for "just refresh"; use refresh when 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.