Skip to content

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>()

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}');
}
}

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'),
);
}
}

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.

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:

many_lints.yaml
rules:
avoid_ref_read_inside_build: false

To keep the rule on but skip certain paths, use per-rule exclude.