Here’s a somewhat embarrassing assumption I made about setState when I started to learn Flutter almost 4 years ago.

We all know setState from the counter example:

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter = _counter + 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text("The current value is $_counter!");
  }
}

Here’s how I thought it worked:

  1. There’s one stateful field called _counter.
  2. We also have a Text widget that displays the value of _counter.
  3. Every time we want to update _counter, we also have to wrap it in an anonymous function and pass it to setState. Otherwise, the framework doesn’t know what was updated.

Four months into my Flutter journey, I found out that assumption number three was not true.

We do have to call setState when updating _counter, but there’s absolutely no need to do it in an anonymous callback.

This:

setState(() {
  _counter = _counter + 1;
});

is exactly the same as this:

_counter = _counter + 1;
setState(() {});

The Flutter framework does not magically inspect code inside the anonymous function and then do some diffing to see what changed.

Whenever you call setState, the widget rebuilds - even if nothing changed and the callback passed to setState was empty.

What I had initially assumed about setState was totally made up in my own head.

A peek behind the curtains

Let’s see how the code behind setState looks like:

@protected
void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throw FlutterError.fromParts([/* ... */]);
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
      throw FlutterError.fromParts([/* ... */]);
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw FlutterError.fromParts([/* ... */]);
    }
    return true;
  }());
  _element.markNeedsBuild();
}

Let’s remove the assertions:

@protected
void setState(VoidCallback fn) {
  final dynamic result = fn() as dynamic;
  _element.markNeedsBuild();
}

We can actually boil it down even more:

@protected
void setState(VoidCallback fn) {
  fn();
  _element.markNeedsBuild();
}

All that setState does is calling the provided callback and then the associated element gets scheduled for a new build.

No magical field diffing here.

So why do we have setState then?

Because of UX studies ran by the Flutter team.

They do a lot of it.

Back in 2017 and 2018, there was a GitHub issue label called “first hour”. The Flutter team ran developer UX studies and observed how Flutter newcomers would use Flutter when left on their own for an hour. Those studies would shape future API decisions for Flutter.

Apparently, the setState thing is one of these findings.

From the GitHub issue that made me realize I had been living in a lie:

We used to just have a markNeedsBuild method but we found people called it like a good luck charm – any time they weren’t sure if they needed to call it, they’d call it.

We changed to a method that takes a (synchronously-invoked) callback, and suddenly people had much less trouble with it.

There’s also a StackOverflow answer from Collin Jackson from the Flutter team back then:

When Flutter had a “markNeedsBuild” function, developers ended up just sort of calling it at random times. When the syntax switched to setState((){ }), developers were much more likely to use the API correctly.

So the whole setState API is just a mental trick. And it seems to work - apparently it lead into people building their widgets less often.

I never had problems knowing when to call setState, but I can definitely see myself wondering “who’s Mark and what is he building?”

Mystery solved.

Conclusion

What does all of this mean?

Should we now convert all our setState calls to something like this?

_counter = _counter + 1;
setState(() {});

Probably not.

Even though there’s no difference, calling setState with an anonymous callback feels right.

At this point, the anonymous callback is an established convention and anything else just feels weird.