build() vs render()
In React, the component function (or render() in class components) is called every time state or props change. It must be a pure function: given the same state and props it returns the same JSX, with no side effects inside the body.
Flutter’s build() method follows exactly the same contract. It is called by the framework on every rebuild triggered by setState, a parent rebuild, or an InheritedWidget change. It must be pure and cheap: no network calls, no Future.wait, no heavy computation — just a description of the UI.
The practical rule is identical in both frameworks: describe, don’t do.
Side-by-side comparison
Section titled “Side-by-side comparison”// React — component function is the "render"function UserCard({ userId }) { const [user, setUser] = useState(null);
// Side effects go in useEffect, NOT in render body useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
// render body: pure description only if (!user) return <p>Loading...</p>; return ( <div className="card"> <h2>{user.name}</h2> <p>{user.email}</p> </div> );}class UserCard extends StatefulWidget { final String userId; const UserCard({super.key, required this.userId});
@override State<UserCard> createState() => _UserCardState();}
class _UserCardState extends State<UserCard> { User? _user;
// Side effects go in initState / didUpdateWidget, NOT in build() @override void initState() { super.initState(); _loadUser(); }
Future<void> _loadUser() async { final u = await fetchUser(widget.userId); if (mounted) setState(() => _user = u); }
// build(): pure description only @override Widget build(BuildContext context) { if (_user == null) return const CircularProgressIndicator(); return Card( child: Column(children: [ Text(_user!.name, style: const TextStyle(fontWeight: FontWeight.bold)), Text(_user!.email), ]), ); }}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.teal)),
home: const Scaffold(body: Center(child: RebuildDemo())),
);
}
}
// This widget counts how many times build() is called —
// demonstrating that build() is cheap and runs frequently.
class RebuildDemo extends StatefulWidget {
const RebuildDemo({super.key});
@override
State<RebuildDemo> createState() => _RebuildDemoState();
}
class _RebuildDemoState extends State<RebuildDemo> {
int _counter = 0;
int _buildCount = 0; // incremented inside build to show frequency
@override
Widget build(BuildContext context) {
// Tracking rebuild count — this is the ONLY acceptable "side effect"
// in build(): a synchronous field increment for demo purposes.
_buildCount++;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Counter: $_counter',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('build() called: $_buildCount times',
style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: const Text('Increment (triggers rebuild)'),
),
],
),
);
}
}