Three Trees (Widget/Element/RenderObject)

High PriorityAsked in ~70% of mid-level interviews

4 min read

Flutter Internals

TreeMutable?LifetimeJob
Widget❌ ImmutableOften discarded after a buildDescribe "what" — config / props
Element✅ MutableLong-livedTrack position in tree; diff old vs new widgets; host State; route inherited lookups
RenderObject✅ MutableLong-livedLayout, paint, hit testing

Each widget gets one element (via createElement()), and a RenderObjectWidget also creates one RenderObject (via createRenderObject(context)). Most widgets aren't render objects — Padding, Column, Theme are composition widgets that produce other widgets.


Code in action — what happens on setState

class _CounterState extends State<Counter> {
  int _n = 0;
  void _inc() => setState(() => _n++);

  @override
  Widget build(BuildContext context) => Text('$_n');     // NEW Text widget every build
}

Step by step:

  1. setState marks this Element dirty.
  2. Next frame, Flutter calls build() → returns a new immutable Text widget.
  3. Flutter walks the Element tree, comparing the new widget to the old one at the same position.
  4. Same runtimeType (Text) + key match → reuse the existing Element and update the RenderObject's text.
  5. Different runtimeType → unmount old Element, mount new one (and dispose old RenderObject).

The widget object is throwaway; the heavy RenderObject is usually reused. That's the whole performance trick.


Why keys matter — the Element matching rule

// Without keys: Flutter matches by index → state attaches to wrong row when items reorder
Column(children: items.map((i) => CheckboxTile(item: i)).toList());

// With keys: Flutter matches by (runtimeType + key) → correct identity preserved
Column(children: items.map((i) => CheckboxTile(key: ValueKey(i.id), item: i)).toList());

The Element-matching algorithm is the reason keys exist. Without them, the diff is positional; with them, it's by identity.


How the trees interact at runtime

ActionWidget treeElement treeRenderObject tree
setState()New widgets built in subtreeElements diff, reuse where possibleRenderObjects mutated, NOT recreated
New widget type at same positionNew widgetOld Element unmounted, new one mountedOld RenderObject disposed, new created
GlobalKey moveSame widgetElement moves to new positionRenderObject moves with it
Theme.of(context) changen/aInherited element notifies dependentsn/a

Common mistakes to avoid

// ❌ Holding a reference to a widget thinking it persists
// Widgets are recreated each build — your reference goes stale immediately

// ❌ Treating BuildContext as if it's "the widget"
// BuildContext IS the Element. It outlives any given widget instance.

// ❌ Doing heavy work in build()
// build() runs whenever the parent rebuilds. Cache results in State, not in widgets.

// ❌ Confusing `runtimeType` matching with `==` matching
// Flutter compares Type (and Key), NOT equality. Two different Text() widgets with the
// same text are still "same Element" if they're at the same position.

// ❌ Using GlobalKey casually for state lookup
// GlobalKeys are expensive and have lifecycle gotchas — prefer InheritedWidget/Provider

Interview follow-ups

  1. Why are widgets immutable? Because they're recreated on every build and the diff must be predictable. Immutability lets Flutter rebuild safely without worrying about widget identity changing mid-frame, and it unlocks const canonicalisation — two const SizedBox(width: 16) calls return the same instance.

  2. What's the difference between runtimeType and Key in Element matching? When Flutter walks the tree comparing old to new, an Element is reused if the new widget has the same runtimeType AND the same Key at that position. Different runtimeType → always remount. Same runtimeType but different keys → also remount. Same of both → update in place.

  3. When does an Element become a RenderObjectElement vs a ComponentElement? Render-object widgets (the leaves that actually paint — RenderObjectWidget subclasses like RenderObjectWidget, LeafRenderObjectWidget, SingleChildRenderObjectWidget) get RenderObjectElements. Composition widgets (everything StatelessWidget / StatefulWidget) get ComponentElements, which don't have a RenderObject — they delegate to children.

  4. How does the State object survive across rebuilds? The State lives on the Element, not on the widget. When the parent rebuilds and produces a new StatefulWidget config, Flutter calls state.updateConfig(newWidget) (effectively widget = newWidget) without recreating the State. State only goes away when the Element is unmounted.


How helpful was this content?

Please sign in to rate this article.