Skip to content

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.

A widget’s build() is called when:

  1. setState() is called on its State object.
  2. Its parent rebuilds (and passes a new widget configuration).
  3. An InheritedWidget it 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
// React: opt-out of re-render with React.memo
const Header = React.memo(function Header({ title }) {
return <h1>{title}</h1>;
});
// React: stable reference with useMemo
function Page() {
const icon = useMemo(() => <Icon name="star" />, []);
return <div>{icon}<Content /></div>;
}
Flutter
// Flutter: const widget is NEVER rebuilt if parent rebuilds
class 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
],
);
}
}

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
// React — key on list items prevents state mixing
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} /> // key by stable id
))}
</ul>
);
}
Flutter
// Flutter — Key on list items
class 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(),
);
}
}
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),
      ),
    );
  }
}
What does marking a Flutter widget constructor const guarantee during rebuilds?
Which Key type is equivalent to React's key={item.id} pattern for stable list items?
What happens when you use UniqueKey() on a widget in Flutter?
Without a Key on list items, how does Flutter match widgets during reconciliation?