Skip to content

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.

React
// 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>
);
}
Flutter
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),
]),
);
}
}
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)'),
          ),
        ],
      ),
    );
  }
}
What is the Flutter equivalent of the React rule "no side effects in the render body"?
Why must you check `if (mounted)` before calling setState after an await?
How do you access the StatefulWidget's configuration fields from inside the State class?