The Problem: Many Flutter developers overuse setState(), calling it even when variable changes don't affect the UI. Every unnecessary setState() is a potential drop from 60 FPS to 59 FPS in complex interfaces.
Chapter 1: When setState Is Actually Unnecessary
Let’s examine a classic example found in 80% of applications: data entry forms. Developers often write:
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
String _username = '';
String _password = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) {
setState(() { // ❌ UNNECESSARY!
_username = value; // This variable is NOT used in UI
});
},
),
TextField(
onChanged: (value) {
setState(() { // ❌ UNNECESSARY!
_password = value; // This variable is also NOT in UI
});
},
),
ElevatedButton(
onPressed: () {
// Send data to server
_sendToServer(_username, _password);
},
child: Text('Submit'),
),
],
);
}
void _sendToServer(String user, String pass) {
// Send data...
}
}
What’s wrong here? Every character typed triggers a complete widget rebuild, even though nothing visually changes! The user sees just the cursor blinking, but under the hood Flutter rebuilds the entire widget tree.
The right approach: If data is only needed for logic or server submission — store it without UI binding:
class _MyFormState extends State<MyForm> {
// These variables are NOT used in build()
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void initState() {
super.initState();
// You can add listeners for additional logic
_usernameController.addListener(() {
print('Username changed: ${_usernameController.text}');
// You can do validation, debounce, etc. here
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _usernameController),
TextField(controller: _passwordController),
ElevatedButton(
onPressed: () {
// Take data directly from controllers
_sendToServer(
_usernameController.text,
_passwordController.text,
);
},
child: Text('Submit'),
),
],
);
}
}
Key principle: If a variable isn’t displayed in build() and doesn't affect how other widgets are displayed — it's not part of UI state and doesn't need setState().
Chapter 2: When setState IS Needed — Do It Precisely
There are cases where a variable truly affects UI, but that doesn’t mean you need to rebuild everything. Let’s look at smarter approaches.
❌ Typical case of excessive setState:
class ProductCard extends StatefulWidget {
@override
_ProductCardState createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard> {
bool _isFavorite = false;
bool _isInCart = false;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network('...'),
Text('Product Name'),
Row(
children: [
IconButton(
icon: Icon(
_isFavorite ? Icons.favorite : Icons.favorite_border,
color: _isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
setState(() { // ❌ Rebuilds the ENTIRE Card!
_isFavorite = !_isFavorite;
});
},
),
IconButton(
icon: Icon(
_isInCart ? Icons.shopping_cart : Icons.add_shopping_cart,
color: _isInCart ? Colors.green : Colors.grey,
),
onPressed: () {
setState(() { // ❌ Rebuilds the ENTIRE Card again!
_isInCart = !_isInCart;
});
},
),
],
),
],
),
);
}
}
Problem: We’re changing only the button color and icon, but rebuilding the entire product card with image, text, and all other elements!
✅ Solution 1: ValueNotifier for precise updates
class ProductCard extends StatelessWidget {
final ValueNotifier<bool> _isFavorite = ValueNotifier<bool>(false);
final ValueNotifier<bool> _isInCart = ValueNotifier<bool>(false);
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network('...'), // ❌ This part WON'T rebuild
Text('Product Name'), // ❌ This either
Row(
children: [
// ✅ Only this icon will rebuild
ValueListenableBuilder<bool>(
valueListenable: _isFavorite,
builder: (context, isFavorite, child) {
return IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
_isFavorite.value = !isFavorite; // Just change value
},
);
},
),
// ✅ And this icon too
ValueListenableBuilder<bool>(
valueListenable: _isInCart,
builder: (context, isInCart, child) {
return IconButton(
icon: Icon(
isInCart ? Icons.shopping_cart : Icons.add_shopping_cart,
color: isInCart ? Colors.green : Colors.grey,
),
onPressed: () {
_isInCart.value = !isInCart;
},
);
},
),
],
),
],
),
);
}
}
What changed:
- Product image and text no longer rebuild on click
- Only the specific icon updates
- Code became more declarative
✅ Solution 2: Extract changing part into separate StatefulWidget
class FavoriteButton extends StatefulWidget {
@override
_FavoriteButtonState createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
bool _isFavorite = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
_isFavorite ? Icons.favorite : Icons.favorite_border,
color: _isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
setState(() {
_isFavorite = !_isFavorite; // Only button rebuilds
});
},
);
}
}
// Use in main widget
class ProductCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network('...'),
Text('Product Name'),
Row(
children: [
FavoriteButton(), // Isolated state
CartButton(), // Another isolated state
],
),
],
),
);
}
}
Chapter 3: Checklist and Practical Rules
The 5-Second Rule
Before writing setState(), ask yourself:
“If I change this variable, will the user see visual changes within the next 5 seconds?”
If no (data goes to server, saves to DB, used for calculations) — you don’t need setState().
Simple Test for Your Code
Try temporarily commenting out setState():
onChanged: (value) {
// setState(() { // Commented out
_someValue = value; // Data still saves!
// });
}
If UI works correctly — setState was unnecessary.
Strategy Guide:
- Variable doesn’t affect UI → simple assignment
_apiToken = 'new_token'; // For sending requests
_userData = json; // For processing
_cache = data; // For storage
2 Affects one small element → ValueNotifier + ValueListenableBuilder
3 Affects several related widgets → extract to separate StatefulWidget
4 Affects entire screen/widget → setState() (use rarely and deliberately)
Typical Scenarios Without setState:
// Logging and analytics
onPressed: () {
_clickCounter++; // Just a variable
_logEvent('button_click');
}
// Timers for logic
Timer.periodic(Duration(seconds: 30), (timer) {
_lastUpdate = DateTime.now(); // For internal logic
_checkForUpdates();
});
// Data caching
Future<void> _loadData() async {
final data = await api.fetchData();
_cachedData = data; // Save, but don't rebuild
_processData(data);
}
Conclusion: Ultimate Cheatsheet
When NOT to use setState:
• Data sent to server
• Saved to database/cache
• Used for logic/calculations
• For analytics/logging
• Temporary processing variables
When setState needed, but precisely:
• Button color/icon changes → ValueNotifier
• Text field updates → separate widget
• UI element state toggles → isolated state
When setState is justified:
• Large portion of screen changes
• Complex animation updates
• Entire list/table rebuilds
Remember: Flutter is not React. You don’t need to update state on every change. Separate UI state from business logic, and your app will run smoothly even on low-end devices.
Every unnecessary setState() is a step from "works fine" to "slideshow mode." Be intentional about state management!


Top comments (0)