State Management
Managing state in your Nocterm apps
State is data that changes over time. This guide shows you how to manage state in Nocterm apps using setState() and other patterns.
Basic usage
Call setState() with a function that updates your state:
class Counter extends StatefulComponent {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Component build(BuildContext context) {
return Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.space) {
_increment();
return true;
}
return false;
},
child: Text('Count: $_count'),
);
}
}
When you call setState():
- The function you pass runs and updates state
- Nocterm marks the component as needing rebuild
- The
build()method runs with the new state - The UI updates
What setState does
setState() schedules a rebuild. It doesn't rebuild immediately—Nocterm batches updates for efficiency.
Update state inside the setState() function:
setState(() {
_count++;
});
The function you pass to setState() should be fast. Don't do expensive work inside it.
When to call setState
Call setState() whenever you change state that affects the UI:
- User input changes a value
- A timer fires and updates time
- An async operation completes and returns data
- Any mutable field in your
Stateclass changes
Don't call setState() during build(). This creates an infinite loop.
setState and async operations
You can call setState() after async operations:
void _loadData() async {
final data = await fetchData();
if (mounted) { // Check if still in tree
setState(() {
_data = data;
});
}
}
Always check mounted before calling setState() in async callbacks. The component might be removed from the tree before the operation completes.
Example: Shared counter
Two components that both need a counter:
class App extends StatefulComponent {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int _count = 0;
void _increment() {
setState(() => _count++);
}
void _decrement() {
setState(() => _count--);
}
@override
Component build(BuildContext context) {
return Column(
children: [
CounterDisplay(count: _count),
SizedBox(height: 1),
CounterControls(
onIncrement: _increment,
onDecrement: _decrement,
),
],
);
}
}
class CounterDisplay extends StatelessComponent {
const CounterDisplay({required this.count, super.key});
final int count;
@override
Component build(BuildContext context) {
return Text('Count: $count');
}
}
class CounterControls extends StatelessComponent {
const CounterControls({
required this.onIncrement,
required this.onDecrement,
super.key,
});
final VoidCallback onIncrement;
final VoidCallback onDecrement;
@override
Component build(BuildContext context) {
return Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.arrowUp) {
onIncrement();
return true;
} else if (event.logicalKey == LogicalKey.arrowDown) {
onDecrement();
return true;
}
return false;
},
child: Text('Use arrows to change'),
);
}
}
State lives in the parent. Children receive data through parameters and send changes up through callbacks.
When to lift state
Lift state when:
- Multiple components need the same data
- Components need to communicate with each other
- You want to share state across different parts of the UI
Keep state as low in the tree as possible. Only lift it when necessary.
Initialize state in initState
Use initState() to set up state that depends on context or needs initialization:
@override
void initState() {
super.initState();
_controller = TextEditingController();
_timer = Timer.periodic(Duration(seconds: 1), _onTick);
}
Always call super.initState() first.
Clean up in dispose
Use dispose() to release resources:
@override
void dispose() {
_controller.dispose();
_timer.cancel();
super.dispose();
}
Always call super.dispose() last.
Keep state minimal
Only store data that affects the UI. Don't store derived values:
// Good
class _CounterState extends State<Counter> {
int _count = 0;
@override
Component build(BuildContext context) {
final isEven = _count % 2 == 0; // Calculate in build
return Text('Count: $_count (${isEven ? 'even' : 'odd'})');
}
}
// Bad - don't store derived values
class _CounterState extends State<Counter> {
int _count = 0;
bool _isEven = true; // Derived from _count
void _increment() {
setState(() {
_count++;
_isEven = _count % 2 == 0; // Extra work to keep in sync
});
}
}
Avoid setState in build
Never call setState() during build():
// Wrong - infinite loop
@override
Component build(BuildContext context) {
setState(() => _count++); // Don't do this!
return Text('Count: $_count');
}
If you need to respond to changes, use lifecycle methods like didUpdateComponent() or didChangeDependencies().
Advanced: Riverpod
For complex state management, Nocterm supports Riverpod. Riverpod provides:
- Global state accessible from anywhere
- Reactive dependencies
- Automatic cleanup
This is an advanced topic. For most apps, setState() and lifting state up are sufficient.
See the Riverpod documentation for details on using providers with Nocterm.
Next steps
- Hot Reload - Fast development with hot reload
- Components - Review component types
- Building UIs - Compose components