Rebuilds & Keys
React uses a virtual DOM diffing algorithm to decide which components to update. Flutter uses a similar element tree reconciliation, but with a few key differences in how you control it — primarily through const constructors and Keys.
Understanding when Flutter rebuilds is essential to writing performant apps. The mental model is close to React, but the tools differ.
When does Flutter rebuild?
Section titled “When does Flutter rebuild?”A widget’s build() is called when:
setState()is called on itsStateobject.- Its parent rebuilds (and passes a new widget configuration).
- An
InheritedWidgetit depends on changes (e.g.,Theme,MediaQuery).
In React, the equivalent triggers are: a state setter call, a parent re-render, or a context value change.
const widgets — the zero-cost optimization
Section titled “const widgets — the zero-cost optimization”In React you reach for React.memo, useMemo, and useCallback to prevent unnecessary re-renders. In Flutter you reach for const — and it is more powerful because it works at the language/compiler level.
A const widget is a compile-time constant. Flutter’s element reconciliation checks identity: if the widget reference in the new build output is identical to the previous one (same object in memory), the element subtree is skipped entirely. const guarantees this identity.
// React: opt-out of re-render with React.memoconst Header = React.memo(function Header({ title }) { return <h1>{title}</h1>;});
// React: stable reference with useMemofunction Page() { const icon = useMemo(() => <Icon name="star" />, []); return <div>{icon}<Content /></div>;}// Flutter: const widget is NEVER rebuilt if parent rebuildsclass Header extends StatelessWidget { final String title; const Header({super.key, required this.title});
@override Widget build(BuildContext context) { return Text(title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)); }}
class Page extends StatelessWidget { const Page({super.key});
@override Widget build(BuildContext context) { return Column( children: [ // This subtree is a compile-time constant — Flutter will // never call its build() again, even if Page rebuilds. const SizedBox(height: 16), const Icon(Icons.star, size: 32), const Header(title: 'My App'), // also const — free skip ], ); }}Keys — identity across rebuilds
Section titled “Keys — identity across rebuilds”React’s key prop tells the reconciler: “this element has this identity — match it to the same element in the previous render, even if its position in the list changed.” Flutter’s Key serves the identical purpose.
Without a Key, Flutter matches widgets by type and position. If you reorder a list of stateful items, Flutter may match the wrong State object to the wrong widget — exactly the same bug as React without list keys.
// React — key on list items prevents state mixingfunction TodoList({ todos }) { return ( <ul> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> // key by stable id ))} </ul> );}// Flutter — Key on list itemsclass TodoList extends StatelessWidget { final List<Todo> todos; const TodoList({super.key, required this.todos});
@override Widget build(BuildContext context) { return ListView( children: todos .map((todo) => TodoItem( key: ValueKey(todo.id), // stable id key todo: todo, )) .toList(), ); }}Runnable example
Section titled “Runnable example”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)),
home: const KeyDemo(),
);
}
}
class KeyDemo extends StatefulWidget {
const KeyDemo({super.key});
@override
State<KeyDemo> createState() => _KeyDemoState();
}
class _KeyDemoState extends State<KeyDemo> {
List<String> _items = ['Apple', 'Banana', 'Cherry'];
bool _useKeys = false;
void _shuffle() => setState(() => _items = [..._items]..shuffle());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Keys Demo')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Text('Use ValueKey:'),
const SizedBox(width: 8),
Switch(
value: _useKeys,
onChanged: (v) => setState(() => _useKeys = v),
),
const Spacer(),
FilledButton(
onPressed: _shuffle,
child: const Text('Shuffle'),
),
],
),
),
Expanded(
child: ListView(
children: _items.map((item) {
// With keys: Flutter matches ColorBox to the same item after shuffle.
// Without keys: Flutter matches by position — colors "jump" to wrong items.
return ColorBox(
key: _useKeys ? ValueKey(item) : null,
label: item,
);
}).toList(),
),
),
],
),
);
}
}
// Stateful widget that holds its own random color as state.
// Without a Key, shuffling reorders the labels but the colors stay
// in their original positions — a classic key-less reconciliation bug.
class ColorBox extends StatefulWidget {
final String label;
const ColorBox({super.key, required this.label});
@override
State<ColorBox> createState() => _ColorBoxState();
}
class _ColorBoxState extends State<ColorBox> {
late final Color _color;
@override
void initState() {
super.initState();
// Random-ish color derived from label hashCode — stable per State instance
final hue = (widget.label.hashCode % 360).abs().toDouble();
_color = HSLColor.fromAHSL(1, hue, 0.6, 0.7).toColor();
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
),
);
}
}