skip to content

In Riverpod 3, what is the difference between ref.invalidate and ref.refresh, and when would you pass asReload: true?

level: middleimportance: should knowfreq 42%

answer

  1. destroy now, rebuild later
  2. refresh equals invalidate plus read
  3. useResult on refresh
  4. several invalidates, one rebuild
  5. asReload defaults to false

basics

~20 s

ref.invalidate discards a provider's state now and lets it rebuild later, coalescing repeated calls; ref.refresh is invalidate followed by read, rebuilding immediately and returning the new value. asReload: true makes an async provider drop its previous data while reloading.

solid answer

~40 s

`ref.invalidate(p)` disposes the provider's current state at once - its `onDispose` callbacks run - and schedules a rebuild for later: typically the next event-loop tick, or not until someone listens again if nobody does. Several `invalidate` calls produce one rebuild, and passing a family invalidates every instance. `ref.refresh(p)` is exactly `invalidate` then `read`: it rebuilds now and returns the value, and it is marked `@useResult`, so when you do not need the value you should call `invalidate`. The typical `refresh` is pull-to-refresh: `onRefresh: () => ref.refresh(quotesProvider.future)`. `asReload` (false by default) matters for async providers: by default the `AsyncValue` keeps the previous data while loading; `asReload: true` turns that off, a hard refresh. If a provider already watches its inputs, you need neither: a currency change rebuilds it automatically.

code

dart · 29 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class QuotesScreen extends ConsumerWidget {
  const QuotesScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return RefreshIndicator(
      onRefresh: () => ref.refresh(quotesProvider.future),
      child: const QuotesList(),
    );
  }
}

class SignOutButton extends ConsumerWidget {
  const SignOutButton({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return TextButton(
      onPressed: () {
        ref.invalidate(quotesProvider, asReload: true);
        ref.invalidate(stockQuoteProvider);
      },
      child: const Text('Sign out'),
    );
  }
}

go deeper

for a junior

Know that invalidate resets a provider and refresh resets it and returns the new value, and that pull-to-refresh uses refresh on the provider's future.

for a middle

Explain the timing difference, coalescing, useResult on refresh, family invalidation and what asReload changes for async providers.

for a senior

Recognise redundant manual invalidation where a watch belongs, and reason about invalidation chains cascading through dependent providers.

for a principal

Define where invalidation is allowed - session boundaries, explicit user refresh - so cache resets stay deliberate rather than scattered through UI handlers.

## Why a provider needs resetting at all Most rebuilds in Riverpod happen by themselves: a provider that `ref.watch`es its inputs re-runs when they change. In a stock-portfolio app, `quotesProvider` watching `currencyProvider` refetches when the user switches currency, with no manual step. **Invalidation** is for the other cases, where no dependency changed but the data is stale: a pull-to-refresh gesture, a retry button after an error, clearing user data on logout. ## invalidate `ref.invalidate(provider, {bool asReload = false})`: - **Destroys the state immediately.** The provider's `onDispose` callbacks run straight away. - **Rebuilds later.** The source says the delay is undefined: typically the next tick of the event loop, but if nothing listens to the provider, the rebuild waits until something does. - **Coalesces.** Calling `invalidate` several times in a row rebuilds once. - **Accepts a family.** Passing the family itself (not one `family(arg)`) invalidates every initialized instance. - **No-op on an uninitialized provider.** - **Returns nothing.** Inside a provider, `ref.invalidateSelf()` does the same to the provider itself, for example from a timer that expires cached quotes. ## refresh `ref.refresh(provider)` is documented as strictly identical to: ```dart ref.invalidate(provider); final newValue = ref.read(provider); ``` It rebuilds synchronously and returns the new value. The method is annotated `@useResult`, so ignoring its return value draws an analyzer warning - the library's way of saying 'use `invalidate` if you do not need the value'. `refresh` accepts a provider or a provider's `.future`, but not a whole family. The canonical use is pull-to-refresh, because `RefreshIndicator.onRefresh` wants a `Future` that completes when the reload is done: ```dart RefreshIndicator( onRefresh: () => ref.refresh(quotesProvider.future), child: const QuotesList(), ) ``` ## Side by side | | `invalidate` | `refresh` | |---|---|---| | When state is discarded | immediately | immediately | | When provider rebuilds | later, possibly only when listened | immediately | | Return value | `void` | the new value (`@useResult`) | | Repeated calls | coalesce into one rebuild | each call rebuilds | | Family argument | allowed, all instances | not allowed | The docs list two benefits of preferring `invalidate` when the value is not needed: it avoids multiple refreshes at once, and it may skip recomputing a provider nobody currently needs. ## asReload For async providers the state is an `AsyncValue`. By default, when an async provider rebuilds after an invalidation, the `AsyncValue` keeps a reference to the previous data during the loading phase, so the UI can keep showing old quotes with a spinner. Passing `asReload: true` disables that and counts as a **hard refresh**: the rebuild is treated as a reload, as if a dependency had changed. Use it when showing the previous data would be wrong - after logout, or when the old values are in a different currency and must not flash on screen. How the UI renders those loading states belongs to `AsyncValue` handling. ## Invalidation chains Invalidating a provider also affects everything that watches it: when it rebuilds with a new value, its dependents re-run. Invalidating a low-level `apiClientProvider` therefore cascades through every provider built on it. The reverse direction is not automatic: invalidating a derived provider does not reset its sources. Riverpod 3 offers an experimental `ref.onManualInvalidation` callback to forward a manual invalidation upstream when that is wanted. ## Common mistakes 1. Calling `invalidate` on a provider that already watches the input that changed - redundant. 2. Using `refresh` and discarding the result, and getting an analyzer warning for it. 3. Expecting `invalidate` to rebuild a provider no widget currently listens to.

  • The user switches currency and the quotes do not update; should you add ref.invalidate(quotesProvider) to the switch handler?
    Usually not. Make `quotesProvider` `ref.watch(currencyProvider)` instead; then every currency change rebuilds it automatically and every other reader stays correct too. Invalidating by hand from a handler couples UI code to the dependency graph and breaks the first time another screen changes the currency.
  • Does invalidating a provider rebuild the providers that watch it?
    Yes, once it rebuilds with a new value, every provider that watches it is notified and re-runs, so invalidating a low-level client provider cascades through the graph. It does not work upward: invalidating a derived provider leaves its sources untouched, unless you forward the invalidation with the experimental `ref.onManualInvalidation`.

saying these in an interview costs you the question

  • invalidate rebuilds the provider synchronously before returning
  • Calling invalidate three times triggers three rebuilds
  • refresh keeps the old state and only fetches new data in the background
  • You must invalidate a provider after changing an input it watches
  • asReload: true is needed for every synchronous provider reset