Explain why build methods must not have side effects - #188939
theprantadutta wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the documentation in editable_text.dart and framework.dart to clarify the side effects of setting TextEditingController.text and executing build methods, introducing a reusable template for build method side-effect warnings. The review feedback suggests expanding the documentation for TextEditingController.text to warn about potential infinite loops when modifying the text property from within its own listener, providing a code snippet to illustrate a safe pattern.
| /// build that sets the text again. This property can be set from a listener | ||
| /// added to this [TextEditingController]. |
There was a problem hiding this comment.
This statement, while correct, can be a bit of a footgun. A common mistake is to set the controller's text from within its own listener, which can easily lead to an infinite loop of notifications.
Since this whole documentation block is about preventing unexpected behavior, it would be beneficial to add a word of caution here. I'd suggest separating this point into its own paragraph and expanding on the potential pitfalls and how to avoid them.
/// build that sets the text again.
///
/// This property can be set from a listener added to this [TextEditingController].
/// When doing so, care should be taken to avoid infinite loops. A listener that
/// sets text will be called again, so a common pattern is to first remove the
/// listener, then set the text, and then re-add the listener.There was a problem hiding this comment.
Good call — added a paragraph in 1ffa759 warning about the listener-loop footgun and suggesting the remove-set-re-add pattern to avoid it.
LongCatIsLooong
left a comment
There was a problem hiding this comment.
Thanks for contributing. But the explanation feels way too lengthy which makes it look a bit daunting.
| /// [TextEditingController], but take care to avoid an infinite loop: setting | ||
| /// the text notifies listeners, so a listener that sets the text will be | ||
| /// triggered again. A common way to avoid this is to remove the listener | ||
| /// before setting the text and re-add it afterward. |
There was a problem hiding this comment.
I think the explanation is too lengthy. Also using this method in production code is already discouraged:
/// This setter is typically only used in tests, as it resets the cursor
There was a problem hiding this comment.
Trimmed it down in 613979d. The setter is already documented as "typically only used in tests", so I've cut the explanation to the part that isn't obvious from that — that setting it notifies listeners, and so it must not happen during build/layout/paint. The infinite-loop paragraph is gone; it was over-explaining a setter that production code shouldn't be reaching for anyway.
| /// any side effects beyond building a widget. The framework decides when and | ||
| /// how often to call it, so any other work done here (mutating state outside | ||
| /// the widget, notifying listeners, starting asynchronous operations, and so | ||
| /// on) would be repeated at unpredictable times, possibly as often as once |
There was a problem hiding this comment.
This sounds like this is describing idempotency instead of being "side-effect-free" (it's a bit pedantic but I think idempotency is the more accurate right term here?)
There was a problem hiding this comment.
You're right, and it's not pedantic — idempotency is the accurate constraint. "No side effects" describes the symptom, but the actual requirement is that the framework may call build arbitrarily often, so calling it repeatedly has to have the same effect as calling it once. Reworded to lead with that in 613979d, which also let me drop the enumeration of side effects and shorten the whole block.
|
Rebased onto Resolved by keeping both sides rather than either one:
Both files analyze clean and are |
| /// this value should only be set between frames, e.g. in response to user | ||
| /// actions, not during the build, layout, or paint phases. This property can | ||
| /// be set from a listener added to this [TextEditingController]. | ||
| /// that they need to update (it calls [notifyListeners]), so it should only |
There was a problem hiding this comment.
The additions here don't add too much value, I recommend swapping them out with a reference to the build docs.
There was a problem hiding this comment.
Good call — done in b46d414. I've reverted the prose here to exactly what was on master and replaced my additions with a pointer instead:
/// See also:
///
/// * [StatelessWidget.build], which explains why a build method must not
/// have side effects such as setting this value.That keeps the explanation in one place (the {@template flutter.widgets.StatelessWidget.build.noSideEffects} block this PR adds in framework.dart, which State.build already pulls in via {@macro}) rather than restating a shortened version of it here. The editable_text.dart side of the diff is now just those five lines.
I also merged master in — the branch had drifted 186 commits and was carrying a stale .ci.yaml, which was enough to fail presubmit on a sibling PR of mine.
The docs for
TextEditingController.text,StatelessWidget.build, andState.buildall state that they must not be set / must not have side effects during build, but none of them explained why ΓÇö which is what #46728 asked for, and Hixie agreed upthread ("We can fix it as part of this issue"). I proposed this scope on the issue on May 13 and there have been no objections since.This PR expands all three doc comments to explain the mechanism:
TextEditingController.text: listeners are typically used to rebuild widgets (attached text fields rebuild their content, other listeners often callsetState); the framework does not allow widgets to be marked as needing to build during the build/layout/paint phases and throws when a listener attempts it; and setting the text from a build method risks rebuilding in a loop.StatelessWidget.build/State.build: expands the single "should not have any side effects beyond building a widget" sentence into general guidance ΓÇö why side effects in build are repeated at unpredictable times, why triggering rebuilds from build is forbidden, and where such logic belongs instead (State.initState,State.didUpdateWidget, event handlers). The shared text lives in a{@template}onStatelessWidget.buildso the two docs stay in sync.Docs-only change, no behavior changes ΓÇö requesting a test exemption.
Fixes #46728
Pre-launch Checklist
///).