Ref rules: watch, read, listen, invalidate
The official docs describe Ref as the primary way to interact with providers — a role comparable to Flutter's BuildContext.
Four core methods and where they belong:
- `ref.watch` — declarative listening. The docs say plainly it is the most common way to listen to providers, and should be your go-to choice. Where: a widget's
buildand a provider's body. - `ref.read` — reading the current value without subscribing. Where: only in user interactions such as button callbacks.
- `ref.listen` — reacting to a change with a side effect: showing a dialog, navigating, logging. It is safe inside
build; outsidebuild(ininitState) useref.listenManual. - `ref.onDispose` — cleanup when the provider is disposed (subscriptions, timers, controllers).
To reset state: ref.invalidate(provider) discards it and recomputes on the next read; ref.refresh(provider) is, in the docs' words, syntax sugar for invalidate plus read.
A specific warning from the docs: do not use `ref.read` as a means to "optimize" your code by avoiding `ref.watch` — this will make your code more brittle. That is the official answer to the frequently asked "can I use read for performance?": no, because read does not see the change and the UI silently keeps a stale value.
class TodoPage extends ConsumerWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch: deklarativ oxunuş, dəyişiklikdə rebuild.
final todos = ref.watch(todosProvider);
// listen: yan effekt — build içində təhlükəsizdir.
ref.listen(todosProvider, (previous, next) {
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Yükləmə alınmadı')),
);
}
});
return Scaffold(
body: switch (todos) {
AsyncValue(:final value?) => TodoList(items: value),
AsyncValue(:final error?) => ErrorView(message: '$error'),
_ => const Center(child: CircularProgressIndicator()),
},
floatingActionButton: FloatingActionButton(
// read: istifadəçi hərəkəti, abunəlik lazım deyil.
onPressed: () => ref.read(todosProvider.notifier).add('Yeni tapşırıq'),
child: const Icon(Icons.add),
),
);
}
}
// Provider daxilində: watch → asılılıq, onDispose → təmizləmə.
final chatProvider = StreamProvider<List<Message>>((ref) {
final socket = ref.watch(socketProvider);
final controller = socket.subscribe('chat');
ref.onDispose(controller.close);
return controller.stream;
});Each method in its place.
| Method | Subscription | Where it is allowed | Typical use |
|---|---|---|---|
| `ref.watch` | Yes — rebuild or recompute on change | `build`, a provider's body | Displaying a value, declaring a dependency |
| `ref.read` | No | Callbacks (`onPressed`, `onTap`) | Calling a method on a notifier |
| `ref.listen` | Yes, but it does not rebuild | `build` (outside: `listenManual`) | A snackbar, dialog, navigation, logging |
| `ref.invalidate` / `ref.refresh` | — | Callbacks, notifier methods | Reloading, pull-to-refresh |
The official DO/DON'T rules (worth quoting directly in an interview):
- Avoid initialising a provider from a widget — the provider should initialize itself; otherwise you risk race conditions.
- Avoid using providers for ephemeral state (form state, the currently selected item, controllers).
- Don't perform side effects during provider initialisation (such as submitting a form) — a provider represents a read operation.
- Use
ref.watch/read/listenwith statically known providers — passing a provider as a parameter defeats static analysis and lints. - Create providers only as top-level
finalvariables.
📚 Sources and documentation
- Ref: watch, read, listenofficialriverpod.dev
The official purpose of each method and the warning against using read as an optimisation.
- DO / DON'Tofficialriverpod.dev
The source of every rule listed above.
- Pull-to-refreshofficialriverpod.dev
The ref.refresh(provider.future) and RefreshIndicator example.
- Cancelling a requestofficialriverpod.dev