setState vs useState
In React, useState is a hook that stores a value and returns a setter. Call the setter, React schedules a re-render, the component function runs again with the new value. The state lives in a hidden slot attached to the component instance.
Flutter’s equivalent is setState(() { ... }). But before you can call it, you need two classes instead of one — a StatefulWidget and a State<T>. That split looks verbose at first, but it solves a specific problem: widget objects in Flutter are immutable and may be recreated cheaply by the framework. The State object is longer-lived and holds the actual mutable data.
Side-by-side comparison
Section titled “Side-by-side comparison”import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </div> );}class Counter extends StatefulWidget { const Counter({super.key});
@override State<Counter> createState() => _CounterState();}
class _CounterState extends State<Counter> { int _count = 0; // state lives here
void _increment() { setState(() { // wraps the mutation _count++; }); }
@override Widget build(BuildContext context) { return Column( children: [ Text('Count: $_count'), ElevatedButton( onPressed: _increment, child: const Text('Increment'), ), ], ); }}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(
title: 'Counter',
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo)),
home: const Scaffold(
body: Center(child: Counter()),
),
);
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() => setState(() => _count++);
void _decrement() => setState(() => _count--);
void _reset() => setState(() => _count = 0);
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_count',
style: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
color: scheme.primary,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(onPressed: _decrement, child: const Text('−')),
const SizedBox(width: 12),
FilledButton(onPressed: _increment, child: const Text('+')),
const SizedBox(width: 12),
OutlinedButton(onPressed: _reset, child: const Text('Reset')),
],
),
],
);
}
}