In Flutter, which part of the tree rebuilds when a State calls setState, and why keep ephemeral state in the smallest widget that needs it?
answer
- one State, its own build
- down the subtree, never up
- identical child instances skipped
- push the state to the leaves
- extract a small StatefulWidget
basics
~20 ssetState reruns build for that one State, and the widgets it returns update down its subtree; ancestors and siblings are untouched. Keeping state in the smallest widget that needs it keeps each rebuild small, which Flutter's docs call pushing state to the leaves.
solid answer
~50 s`setState` runs its callback synchronously and marks that `State`'s element as needing a build, so on the next frame only that `State`'s `build` runs. The widgets it returns are then compared with the previous ones and updated, which can cascade through its **subtree**; a child that is the identical widget instance — a `const` widget or one passed in from above — is skipped. Nothing above it or beside it rebuilds, and a child cannot rebuild its parent except by calling a callback the parent passed down. So placement sets scope: if a sign-in screen keeps the password toggle in its own `State`, every tap rebuilds the whole form; extracting a `PasswordField` `StatefulWidget` confines each tap to one field. The `StatefulWidget` docs give the same advice — push state to the leaves, for example a dedicated clock widget rather than a ticking page.
code
dart · 59 linesimport 'package:flutter/material.dart';
class SignInScreen extends StatefulWidget {
const SignInScreen({super.key});
@override
State<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
final _email = TextEditingController();
final _password = TextEditingController();
@override
void dispose() {
_email.dispose();
_password.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// No show/hide flag here: toggling it must not rebuild the whole form.
return Column(
children: <Widget>[
const FlutterLogo(size: 64),
TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email')),
PasswordField(controller: _password), // owns its own _obscured bool
],
);
}
}
class PasswordField extends StatefulWidget {
const PasswordField({super.key, required this.controller});
final TextEditingController controller;
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscured = true;
@override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
obscureText: _obscured,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(_obscured ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscured = !_obscured), // rebuilds only this field
),
),
);
}
}go deeper
Know that setState reruns this State's build and its subtree, never its parents, and that small StatefulWidgets keep rebuilds small.
Explain how returned children are compared, why identical instances are skipped, and how to extract widgets or pass child subtrees to narrow scope.
Review screens for state held too high, and restructure them so frequent interactions rebuild only the widgets that display the change.
Make scope-conscious placement a team convention that keeps code readable, and reserve measurement-driven optimisation for screens that need it.
## What setState actually schedules A `State` object calls `setState(() { … })` to say "my data changed". The framework's implementation is small: it runs the callback **synchronously**, then marks the `State`'s **element** as needing a build. On the next frame the framework calls that `State`'s `build` method again. How the element is marked dirty and when the frame runs are covered with the widget lifecycle; what matters here is **how far the change reaches**. ## How far a rebuild reaches 1. **The State's own `build` runs.** It returns a fresh tree of widget objects. 2. **Each returned child is compared with the previous one.** If the child is the *identical* widget instance — a `const` widget, or a widget object handed in from the parent — the framework keeps the existing element and does not rebuild it. 3. **Otherwise the child is updated**, and a stateless child's `build` or a stateful child's `State.build` runs in turn. This is how one `setState` can cascade through the entire subtree below it. 4. **Nothing above or beside it rebuilds.** Ancestors and siblings are not touched. The framework's own documentation warns that the indirect cost of `setState` is high for exactly this reason: it can trigger rebuilds for the entire subtree rooted at that widget, followed by layout and paint of the corresponding render objects. ## Placement decides scope Because a rebuild starts at the `State` that owns the data, **where ephemeral state lives decides how much rebuilds**. Compare two versions of a notes app's sign-in screen: | Where the show/hide bool lives | What rebuilds on each tap | |---|---| | `_SignInScreenState` | the whole screen: logo, email field, password field, buttons | | `_PasswordFieldState` in an extracted `PasswordField` widget | only the password field | The `StatefulWidget` documentation calls this **pushing the state to the leaves**, with the example of a ticking clock: rather than keeping the time at the top of a page and rebuilding the page every second, create a dedicated clock widget that only updates itself. ## Techniques for narrowing scope - **Extract a `StatefulWidget`** around the smallest piece of UI that uses the value. - **Pass unchanging subtrees in as `child`** arguments, so the owner's `build` returns the same instance and the framework skips it. - **Use `const` constructors** for static parts of the returned tree. - **Keep controllers where they are used**, for example a `TextEditingController` owned by the screen that submits the form, not by the app. Measuring whether a rebuild is actually expensive is a performance topic; the placement habit is cheap and keeps code clearer regardless. ## What setState cannot do - It **cannot reach up**. A child that needs its parent to change calls a callback the parent passed down (`VoidCallback`, `ValueChanged<T>`), and the parent calls its own `setState`. - It **cannot reach sideways**. Two sibling widgets that must stay in sync need their shared value lifted into a common ancestor. - It **cannot outlive its `State`**. When the widget leaves the tree — for example, the sign-in route is popped — the `State` is disposed and its fields are gone. Those limits are what separate ephemeral state, which fits comfortably inside one `State`, from app state, which needs an owner above everyone who reads it. ## Reading the scope in practice The **Track Widget Builds** option in DevTools' Performance view makes scope visible: it adds each widget's `build()` call to the timeline, so toggling the password visibility and inspecting that frame shows what rebuilt. In the screen-level version the logo, email field and buttons appear as build events; in the extracted version only the password field does. The same check works for any interaction that feels heavier than it should — scroll-linked headers, timers, per-keystroke validation — and usually points straight at a `State` holding data that only a small descendant needs. ## Common mistakes 1. Keeping every toggle, animation flag and form field in one screen-level `State`, so any interaction rebuilds the whole screen. 2. Calling `setState` in a child and expecting the parent to redraw. 3. Wrapping an assignment that does not affect `build` in `setState`, which schedules work for nothing. 4. Duplicating a value in both parent and child `State` "for speed", which creates two copies that drift apart.
- Why does a const widget returned from build not rebuild when its parent's State calls setState?The framework compares each returned child with the previous one, and a `const` expression yields the identical instance every time. When the old and new widget are the same object, the existing element is kept and that child's build is skipped. A child passed in from above as a constructor argument benefits the same way.
- A child widget calls setState, but the parent's summary text does not change. Why, and what is the fix?`setState` only schedules a build for the `State` that calls it and what lies below it; it never reaches ancestors. Lift the value into the parent's `State`, pass it down, and give the child a callback such as `ValueChanged<bool>` that the parent implements with its own `setState`.
saying these in an interview costs you the question
- setState rebuilds the entire app from the root widget.
- setState in a child also rebuilds its parent and siblings.
- Only the single widget whose field changed rebuilds; its children never do.
- Keeping all screen state in one top-level State has no rebuild cost.
- A const child is rebuilt every time its parent's build runs.