Effects & Batching
Basic Effect
dart
final count = signal(context, 0);
final stop = effect(context, () {
debugPrint('count = ${count()}');
});
// stop the effect manually
stop();Cleanup Hooks
Use onEffectCleanup for re-run cleanup, and onEffectDispose for final cleanup.
dart
effect(context, () {
onEffectCleanup(() => debugPrint('cleanup before re-run'));
onEffectDispose(() => debugPrint('dispose'));
});Widget Lifecycle Hooks
Use these hooks to run one-time effects tied to a widget element's lifecycle. They are lightweight helpers built on memoization and element tracking.
When to use
- Show a one-time toast/snackbar after the first frame.
- Start or schedule work once and dispose/cancel it when the widget leaves.
- Attach listeners that should live as long as the widget element does.
When not to use
- If you need reactive re-runs based on signals, use
effect. - If you need cleanup for an
effect, useonEffectDispose. - If you need per-build logic, keep it in
build.
Behavior
onMounted: runs once after the first frame for the current element.onUnmounted: runs once when the element is removed from the tree (triggered on the next frame).- Both will run again after a full unmount + remount.
Common pitfalls
- Must be called inside
build(or aBuilder), not across async gaps. - Must be called unconditionally and in the same order every rebuild.
onUnmountedruns on the next frame, so tests mustpump()after removal.
Example
dart
Builder(
builder: (context) {
onMounted(context, () {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Mounted')));
});
final timer = Timer(const Duration(seconds: 5), () {
debugPrint('tick');
});
onUnmounted(context, timer.cancel);
return const SizedBox();
},
);Effect Scope
dart
final scope = effectScope(context, () {
effect(context, () => debugPrint('A'));
effect(context, () => debugPrint('B'));
});
scope(); // dispose all effects in the scopeBatch Updates
dart
batch(() {
a.set(a() + 1);
b.set(b() + 1);
});Flutter Example (from the example app)
dart
class EffectBatchSection extends StatelessWidget {
const EffectBatchSection({super.key});
@override
Widget build(BuildContext context) {
final a = signal<int>(context, 1);
final b = signal<int>(context, 2);
final sum = computed<int>(context, (_) => a() + b());
final effectRuns = signal<int>(context, 0);
effect(context, () {
sum();
final current = untrack(() => effectRuns());
effectRuns.set(current + 1);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('a: ${a()} b: ${b()} sum (computed): ${sum()}'),
Text('effect runs: ${effectRuns()}'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => a.set(a() + 1),
child: const Text('Increment A'),
),
ElevatedButton(
onPressed: () => b.set(b() + 1),
child: const Text('Increment B'),
),
OutlinedButton(
onPressed: () {
batch(() {
a.set(a() + 1);
b.set(b() + 1);
});
},
child: const Text('Batch +1 both'),
),
],
),
],
);
}
}