Skip to content

avoid_unnecessary_setstate

v0.4.0WarningFixConfigurableState Management

Warns when setState is called directly inside initState, didUpdateWidget, or build methods. In these lifecycle methods, mutating state directly is sufficient because the framework already schedules a build after they return. Calling setState in build triggers an unnecessary additional rebuild.

In initState and didUpdateWidget, the framework will call build automatically after the method returns, so wrapping mutations in setState is redundant overhead. In build, calling setState triggers an infinite rebuild loop or at minimum a wasted frame. Event handler callbacks (like onTap) inside build are excluded from this rule since they run asynchronously and do need setState.

See also: setState

Wrapping initial setup in setState. The framework builds this widget as soon as initState returns, so the setState buys nothing and only adds a frame’s worth of bookkeeping:

class _ProfilePageState extends State<ProfilePage> {
String _title = '';
@override
void initState() {
super.initState();
setState(() { // LINT
_title = widget.user.displayName;
});
}
@override
Widget build(BuildContext context) => Text(_title);
}

didUpdateWidget is the same — it is already part of a build:

class _ProfilePageState extends State<ProfilePage> {
String _title = '';
@override
void didUpdateWidget(ProfilePage oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() { // LINT
_title = widget.user.displayName;
});
}
@override
Widget build(BuildContext context) => Text(_title);
}

In build it is worse than redundant: it schedules another build from inside a build, which at best wastes a frame and at worst never settles.

class _ProfilePageState extends State<ProfilePage> {
int _renderCount = 0;
@override
Widget build(BuildContext context) {
setState(() { // LINT
_renderCount++;
});
return Text('$_renderCount');
}
}

Assign directly in the lifecycle methods:

class _ProfilePageState extends State<ProfilePage> {
String _title = '';
@override
void initState() {
super.initState();
_title = widget.user.displayName;
}
@override
void didUpdateWidget(ProfilePage oldWidget) {
super.didUpdateWidget(oldWidget);
_title = widget.user.displayName;
}
@override
Widget build(BuildContext context) => Text(_title);
}

An async method resumes long after its lifecycle method returned, so nothing has scheduled a build for it:

class _ProfilePageState extends State<ProfilePage> {
String _title = '';
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final user = await fetchUser();
if (!mounted) return;
setState(() {
_title = user.displayName;
});
}
@override
Widget build(BuildContext context) => Text(_title);
}

A tap handler written inside build runs after the build finished, so it does need setState — and the rule leaves it alone:

class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) => ElevatedButton(
onPressed: () => setState(() => _count++),
child: Text('$_count'),
);
}

The event-handler exemption keys on the closure being a named argument — onPressed:, onTap:, onChanged:. A callback passed positionally is reported even though it behaves the same way.

Only initState, didUpdateWidget and build are checked. A setState in didChangeDependencies is not reported, though the framework rebuilds after that too.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated. Add it to preset: core with avoid_unnecessary_setstate: true.

To turn it off:

many_lints.yaml
rules:
avoid_unnecessary_setstate: false

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

Projects with a state abstraction that does not extend Flutter’s State can opt that base class into this rule:

analysis_options.yaml
many_lints:
rules:
avoid_unnecessary_setstate:
state_base_classes: [AppState]
Option Type Default Description
state_base_classes list of strings [] Additional non-State base classes whose subclasses should be treated as state classes