When should you use setState()?

High PriorityAsked in ~80% of Flutter interviews

2 min read

State Management

Theory — what setState actually does

setState(fn) does two things:

  1. Runs fn() synchronously — this is where you mutate fields.
  2. 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

SituationUse
Toggle, counter, expanded/collapsedsetState
Form fields, validation flagssetState
Local animation flag, hover statesetState
State needed by sibling widgetsLift state up (Q35), or use Provider/Riverpod
State needed by many screens (auth, cart)Provider / Riverpod / BLoC
Async loaded data, streamsProvider'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

  1. Why mutate state inside the setState callback instead of outside? It's a convention that makes intent explicit and is the only documented contract — Flutter actually marks dirty after fn runs, regardless of where you mutate. Keeping the mutation inside makes diffs and reviews easier and prevents the "I forgot to call setState" bug.

  2. What does if (mounted) do, and why is it needed? mounted is a bool on State that's false once the widget has been disposed. Async work might complete after the user navigates away — calling setState then throws. The guard avoids it.

  3. What's the cost of setState? It rebuilds the State's build() and the subtree it returns. Push your StatefulWidget down to the smallest area that actually changes — that way each setState only rebuilds a tiny piece.

  4. When does setState become 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.