When should you use setState()?
2 min read
State Management
Theory — what setState actually does
setState(fn) does two things:
- Runs
fn()synchronously — this is where you mutate fields. - Marks the State's element as dirty so Flutter rebuilds it on the next frame.
It's not magical — the widget rebuilds because the element was marked dirty, not because Flutter "watched" your variable.
Code in action
class _CounterState extends State<Counter> {
int _count = 0;
void _inc() {
setState(() => _count++); // mutate INSIDE the callback
}
Future<void> _loadInitial() async {
final v = await api.fetchCount();
if (!mounted) return; // ⚠️ guard after async
setState(() => _count = v);
}
@override
Widget build(BuildContext context) => Column(children: [
Text('$_count'),
ElevatedButton(onPressed: _inc, child: const Text('+')),
]);
}
When to reach for setState vs something bigger
| Situation | Use |
|---|---|
| Toggle, counter, expanded/collapsed | setState |
| Form fields, validation flags | setState |
| Local animation flag, hover state | setState |
| State needed by sibling widgets | Lift state up (Q35), or use Provider/Riverpod |
| State needed by many screens (auth, cart) | Provider / Riverpod / BLoC |
| Async loaded data, streams | Provider's FutureProvider/StreamProvider, or FutureBuilder / StreamBuilder |
| Persisted state (settings) | A repository + state-management layer |
Rule of thumb: if you'd have to pass it through more than two widgets, lift it out of setState.
Common mistakes to avoid
// ❌ Mutating outside setState — UI won't refresh
_count++;
setState(() {}); // works but suggests confusion
// ✅ setState(() => _count++);
// ❌ Calling setState in build()
@override
Widget build(BuildContext context) {
setState(() { ... }); // 💥 infinite rebuild loop
}
// ❌ Calling setState after the widget unmounted
fetch().then((d) => setState(() => _d = d)); // crash if user navigated away
// ✅
if (!mounted) return;
setState(() => _d = d);
// ❌ Doing heavy work inside setState
setState(() {
_bigList = expensiveSort(items); // sorts on the UI thread, blocks frame
});
// ✅ Compute first, then setState with the result
// ❌ Calling setState in dispose()
// ✅ Don't — the widget is going away
// ❌ Using setState for app-wide auth state
// One login flow grows into many screens needing the user → switch to a store
Interview follow-ups
-
Why mutate state inside the
setStatecallback instead of outside? It's a convention that makes intent explicit and is the only documented contract — Flutter actually marks dirty afterfnruns, regardless of where you mutate. Keeping the mutation inside makes diffs and reviews easier and prevents the "I forgot to call setState" bug. -
What does
if (mounted)do, and why is it needed?mountedis aboolonStatethat's false once the widget has been disposed. Async work might complete after the user navigates away — callingsetStatethen throws. The guard avoids it. -
What's the cost of
setState? It rebuilds the State'sbuild()and the subtree it returns. Push your StatefulWidget down to the smallest area that actually changes — that way eachsetStateonly rebuilds a tiny piece. -
When does
setStatebecome a code smell? When the same data must be passed through several widgets, when async loading state proliferates copy-pasted patterns, or when business logic creeps into your State classes. That's the signal to introduce a state manager.
How helpful was this content?
Please sign in to rate this article.