Skip to content

Mental Model Quickstart

Four ideas unlock Flutter for React developers. Once these click, the rest of the framework is just learning the widget API — the mental model is already yours.

In React, your UI has components (your code) and HTML elements (<div>, <p>, <button>). Two distinct layers.

In Flutter there is only one layer: widgets. Layout, spacing, colour, text, images, gesture detection, even the app itself — all widgets. There is no HTML underneath. Flutter draws every pixel directly.

flowchart TB
  subgraph React
    direction TB
    RApp["&lt;App&gt;"] --> RDiv["&lt;div className='card'&gt;"]
    RDiv --> RH2["&lt;h2&gt;Hello&lt;/h2&gt;"]
    RDiv --> RBtn["&lt;button&gt;Click&lt;/button&gt;"]
  end
  subgraph Flutter
    direction TB
    FApp["MyApp (StatelessWidget)"] --> FMaterial["MaterialApp"]
    FMaterial --> FScaffold["Scaffold"]
    FScaffold --> FCenter["Center"]
    FCenter --> FColumn["Column"]
    FColumn --> FText["Text('Hello')"]
    FColumn --> FBtn["ElevatedButton(...)"]
  end
React element tree vs Flutter widget tree

The depth feels greater at first. But it is consistent — there are no two mental models to switch between.

React’s component tree and Flutter’s widget tree are the same concept. Both are descriptions of what should be rendered. Both are re-evaluated when state changes. The framework diffs the description against the previous one and updates only what changed.

React
// React component tree
function Card({ title, body }) {
return (
<div className="card">
<h3>{title}</h3>
<p>{body}</p>
</div>
);
}
function App() {
return (
<main>
<Card title="Flutter" body="Everything is a widget" />
<Card title="React" body="Everything is a component" />
</main>
);
}
Flutter
// Flutter widget tree
class InfoCard extends StatelessWidget {
final String title;
final String body;
const InfoCard({super.key, required this.title, required this.body});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(body),
],
),
),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: const [
InfoCard(title: 'Flutter', body: 'Everything is a widget'),
InfoCard(title: 'React', body: 'Everything is a component'),
],
),
),
);
}
}

Save a file → the running app updates in under a second without losing state. This is Flutter’s Hot Reload, and it works identically to React’s Fast Refresh:

  • Edits to build() methods are injected immediately.
  • Widget state is preserved (the counter keeps its count).
  • Changes to initState(), class fields, or main() require Hot Restart (R) to take effect — just like changes to module-level code in React require a manual refresh.

4. Declarative UI — the same idea you know

Section titled “4. Declarative UI — the same idea you know”

Flutter is declarative. You describe what the UI should look like for a given state — you do not imperatively mutate the DOM. This is the same contract React introduced.

// Imperative (jQuery-style thinking — don't do this)
// "find the Text widget and update its text to '$_count'"
// Declarative (Flutter way)
// "given _count, build this subtree"
Text('Count: $_count')

When setState runs, Flutter calls build() again and the Text widget gets the new value. You describe the outcome, Flutter figures out the minimal update.

Runnable example — all four concepts together

Section titled “Runnable example — all four concepts together”
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

// 1. Everything is a widget (even the app root)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      // 2. Widget tree: MyApp > MaterialApp > Scaffold > ...
      home: const WidgetTreeDemo(),
    );
  }
}

// 4. Declarative: build() describes the UI for the current state
class WidgetTreeDemo extends StatefulWidget {
  const WidgetTreeDemo({super.key});

  @override
  State<WidgetTreeDemo> createState() => _WidgetTreeDemoState();
}

class _WidgetTreeDemoState extends State<WidgetTreeDemo> {
  int _likes = 0;
  bool _bookmarked = false;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Mental Model Demo'),
        backgroundColor: colors.primaryContainer,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 3. Try editing this Text and hot-reloading
            Text(
              'Flutter Mental Model',
              style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
            ),
            const SizedBox(height: 8),
            const Text(
              'Everything is a widget. The tree is declarative. '
              'Hot Reload keeps your state while updating the UI.',
            ),
            const SizedBox(height: 24),
            Row(
              children: [
                FilledButton.icon(
                  onPressed: () => setState(() => _likes++),
                  icon: const Icon(Icons.favorite),
                  label: Text('$_likes Likes'),
                ),
                const SizedBox(width: 12),
                OutlinedButton.icon(
                  onPressed: () => setState(() => _bookmarked = !_bookmarked),
                  icon: Icon(
                    _bookmarked ? Icons.bookmark : Icons.bookmark_border,
                  ),
                  label: Text(_bookmarked ? 'Saved' : 'Save'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
In Flutter, what sits 'below' widgets in the rendering stack?
What happens when `setState()` is called in a StatefulWidget?
In Flutter, how is padding / spacing added around a widget?
Which Flutter feature is equivalent to React's Fast Refresh?