avoid_ref_read_inside_build
v0.4.0 Warning Fix Riverpod State
This rule flags a one-off provider read inside a build() method. Such a read fetches the value once and does not subscribe, so the widget will not rebuild when the provider updates.
It covers both ecosystems, because the mistake is identical in each:
One-off (flagged in build) |
Subscribing (correct in build) |
|
|---|---|---|
| Riverpod | ref.read(...) |
ref.watch(...) |
| package:provider | context.read<T>() |
context.watch<T>() |
Why use this rule
Section titled “Why use this rule”A one-off read in build() is almost always a mistake. The widget renders with whatever value the provider had at that moment and is never told it changed, so the UI goes stale until something unrelated rebuilds it — a bug that reproduces only in the order the user happened to tap things.
A one-off read inside a callback (onPressed) is intentional and is not reported.
See also: Riverpod refs, provider: read vs watch
A cart badge that reads its count once. It renders 3, the user adds an item,
and the badge keeps saying 3 until some unrelated rebuild happens to refresh
it:
final cartCountProvider = StateProvider<int>((ref) => 0);
class CartBadge extends ConsumerWidget { const CartBadge({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final count = ref.read(cartCountProvider); // LINT return Text('$count'); }}The same mistake in package:provider, where the receiver is the BuildContext
rather than a ref:
class CartBadge extends StatelessWidget { const CartBadge({super.key});
@override Widget build(BuildContext context) { final cart = context.read<Cart>(); // LINT return Text('${cart.itemCount}'); }}Subscribe, so the badge is told when the count changes:
final cartCountProvider = StateProvider<int>((ref) => 0);
class CartBadge extends ConsumerWidget { const CartBadge({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(cartCountProvider); return Text('$count'); }}class CartBadge extends StatelessWidget { const CartBadge({super.key});
@override Widget build(BuildContext context) { final cart = context.watch<Cart>(); return Text('${cart.itemCount}'); }}A read inside a callback is fine
Section titled “A read inside a callback is fine”The callback runs on a tap, long after build returned, and wants the value as
it is then — not a subscription:
class AddToCartButton extends ConsumerWidget { const AddToCartButton({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { return ElevatedButton( onPressed: () => ref.read(cartProvider.notifier).add(Item.sample), child: const Text('Add to cart'), ); }}Known limitations
Section titled “Known limitations”Only a method literally named build is checked. A build-time helper you
extracted — Widget _buildHeader(WidgetRef ref) — is not, even though its read
is just as stale.
The two ecosystems are told apart by the receiver’s resolved type, not its
name: Riverpod’s ref is a WidgetRef/Ref, package:provider’s extensions
hang off BuildContext. So widgetContext.read<T>() is caught under any
receiver name, while a field of some unrelated class you happened to call ref
is not.
Configuration
Section titled “Configuration”This rule is in the recommended preset, so it is on with
preset: recommended or preset: opinionated. Add it to preset: core with
avoid_ref_read_inside_build: true.
To turn it off:
rules: avoid_ref_read_inside_build: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_ref_inside_state_dispose— Avoid accessing ref inside the dispose method of a ConsumerState.avoid_ref_watch_outside_build— Subscribe only in build; read once everywhere else.avoid_build_context_in_providers— Providers outlive widgets, so they should not receive a BuildContext.notifier_build— Classes annotated with @riverpod must define a build method.