Lifting State Up
Sharing state between sibling widgets by moving it to their nearest common ancestor.
2 phút đọc
An earlier lesson noted that setState only affects a widget's own subtree — it can't update a sibling. This lesson covers the standard fix: lifting state up to the nearest common ancestor of the widgets that need to share it.
The problem
Imagine a product list and a cart badge, both needing to reflect the same cart count, but neither is a parent of the other:
// This doesn't work: incrementing count inside ProductList
// has no way to reach CartBadge, a sibling widget.
Column(
children: [
ProductList(),
CartBadge(),
],
)If each widget manages _cartCount independently in its own State, they'll inevitably disagree — tapping "add to cart" in ProductList updates only its own copy of the count, leaving CartBadge unaware anything happened.
The fix: move the state up
Instead of each widget owning its own copy, move _cartCount to their shared parent, and pass it down as data plus a callback:
class ShopScreen extends StatefulWidget {
const ShopScreen({super.key});
@override
State<ShopScreen> createState() => _ShopScreenState();
}
class _ShopScreenState extends State<ShopScreen> {
int _cartCount = 0;
void _addToCart() {
setState(() => _cartCount++);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CartBadge(count: _cartCount),
Expanded(
child: ProductList(onAddToCart: _addToCart),
),
],
);
}
}ProductList and CartBadge become simple StatelessWidgets again — CartBadge just displays whatever count it's given, and ProductList calls onAddToCart (a function passed down from the parent) whenever the user taps "add," without knowing or caring how the count is stored:
class CartBadge extends StatelessWidget {
final int count;
const CartBadge({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('Cart: $count');
}
}
class ProductList extends StatelessWidget {
final VoidCallback onAddToCart;
const ProductList({super.key, required this.onAddToCart});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onAddToCart,
child: const Text('Add to cart'),
);
}
}Now there's exactly one source of truth for _cartCount, owned by _ShopScreenState, and both children simply reflect or trigger changes to it. This is "data down, callbacks up" — the widget tree flows data downward through constructor parameters, and events flow upward through callback functions.
Where this breaks down
Lifting state up works well for state shared by a handful of nearby widgets. But if the cart count needs to be read by a screen three levels deeper in the tree, you'd have to pass count and onAddToCart down through every intermediate widget as a parameter, even ones that don't use it themselves — a pattern often called "prop drilling."
// Painful: every intermediate widget has to forward count,
// even ones that never display or use it.
ShopScreen -> CategoryTab -> ProductGrid -> ProductCard -> AddButtonThis is precisely the pain point that dedicated state management approaches solve, which the next lesson introduces. Lifting state up is still the right first move for anything localized — reach for something more powerful only once prop drilling actually starts to hurt.