Skip to content

avoid_ref_inside_state_dispose

v0.4.0 Warning Riverpod State

This rule catches ref usage inside the dispose() method of ConsumerState classes. By the time dispose() runs, providers may already be torn down, so reading or watching them can throw unexpected errors or return stale data.

In Riverpod, the lifecycle of providers and widgets is not tightly coupled. When dispose() fires, there is no guarantee that the providers you are trying to access are still alive. Accessing ref in dispose() can silently read disposed state or throw UnmountedRefException, leading to hard-to-debug crashes in production.

See also: Riverpod automatic disposal

The usual shape: a dispose() that tries to tell a provider the screen is gone. By the time it runs the provider may already have been torn down, so this either throws or writes to state nothing is listening to.

class _EditorPageState extends ConsumerState<EditorPage> {
final _controller = TextEditingController();
@override
void dispose() {
ref.read(draftProvider.notifier).save(_controller.text); // LINT
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TextField(controller: _controller);
}

Dispose only what this widget owns, and push the provider work to the moment the user actually leaves — or let the provider clean up after itself with ref.onDispose:

class _EditorPageState extends ConsumerState<EditorPage> {
final _controller = TextEditingController();
void _saveDraft() {
ref.read(draftProvider.notifier).save(_controller.text);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TextField(
controller: _controller,
onSubmitted: (_) => _saveDraft(),
);
}

Capture it while the widget is alive, then use the captured value in dispose():

class _SessionPageState extends ConsumerState<SessionPage> {
late final Analytics _analytics = ref.read(analyticsProvider);
@override
void dispose() {
_analytics.screenClosed('session'); // no ref here
super.dispose();
}
@override
Widget build(BuildContext context) => const SizedBox();
}

The search stops at a closure boundary. A ref read inside a callback declared in dispose() is not reported, because such a callback may be stored and run somewhere else entirely.

Only dispose() on a ConsumerState subclass is checked. deactivate() has the same hazard and is not covered.

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_inside_state_dispose: true.

To turn it off:

many_lints.yaml
rules:
avoid_ref_inside_state_dispose: false

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