skip to content

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?

level: juniorimportance: should knowfreq 52%

answer

  1. one State, its own build
  2. down the subtree, never up
  3. identical child instances skipped
  4. push the state to the leaves
  5. extract a small StatefulWidget

basics

~20 s

setState 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 lines
dart
import '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

for a junior

Know that setState reruns this State's build and its subtree, never its parents, and that small StatefulWidgets keep rebuilds small.

for a middle

Explain how returned children are compared, why identical instances are skipped, and how to extract widgets or pass child subtrees to narrow scope.

for a senior

Review screens for state held too high, and restructure them so frequent interactions rebuild only the widgets that display the change.

for a principal

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.