Manage state and resources
Use this guide when a widget needs its own value, controller, or external
resource, and you want that concern named and reusable instead of spread over
several State fields and lifecycle methods.
The optional noir_signals package adds SignalWidget, a host that retains
hooks. Each hook owns what it creates and releases it when the widget unmounts.
import 'package:noir/noir.dart';
import 'package:noir_signals/noir_signals.dart';Own a resource and its cleanup together
This widget owns an observed count and a periodic timer. The effect starts the timer and returns the callback that cancels it. Noir runs that callback when the interval changes or the widget leaves the tree:
import 'dart:async';
import 'package:noir/noir.dart';
import 'package:noir_signals/noir_signals.dart';
class PollingBadge extends SignalWidget {
const PollingBadge({required this.interval, super.key});
final Duration interval;
@override
Widget build(BuildContext context) {
final ticks = useState<int>(0);
useEffect(() {
final timer = Timer.periodic(interval, (_) {
ticks.value++;
});
return timer.cancel;
}, <Object?>[interval]);
return Row(
spacing: 1,
children: [
const Badge(label: 'POLLING', variant: BadgeVariant.info),
Text('${ticks.value} checks'),
],
);
}
}useState owns and observes a ValueNotifier. Changing ticks.value requests
a rebuild. The timer callback runs later, after the effect has returned, so it
can update that value safely.
Three rules keep this example correct:
- Call hooks unconditionally, at the top of
build, in the same order on every build. Hooks use call order as identity. - The key list decides when a hook replaces its resource.
[interval]installs a new timer and cancels the old one when the interval changes. An empty list keeps one resource for the life of the mounted slot. No key list at all runs the cleanup and the effect on every build. - An effect runs synchronously during build. It must not change an observed
hook value before it returns. Start external work, return its cleanup, and
handle input in
onPressed.
The hooks contract is the canonical reference for replacement order, cleanup failures, and the runtime guard behind that third rule.
Match the hook to the job
| Need | Start with |
|---|---|
| Observed local value | useState |
| Retained object, Future, or Stream identity | useMemoized |
| Acquire and release an external resource | useEffect |
| Read a Listenable and rebuild | useListenable or useValueListenable |
| Editable text, focus, or scrolling | useTextEditingController, useFocusNode, or useScrollController |
| Time-based values | useAnimationController and useAnimation |
| Mutable bookkeeping that must not rebuild | useRef |
Create a Future or a Stream outside build, or retain it with useMemoized.
Creating one on every build restarts the asynchronous work.
Reuse one concern as a custom hook
A custom hook is a top-level function whose name starts with use. Keep the
owned notifier private when callers need only a value and a callback:
typedef CounterState = ({int value, VoidCallback increment});
CounterState useCounter([int initialValue = 0]) {
final counter = useState(initialValue);
return (value: counter.value, increment: () => counter.value++);
}Use SignalBuilder when a small, stable subtree inside an ordinary
StatefulWidget needs hooks.
When a State class is clearer
Use a State class when several lifecycle methods, related resources, or a
longer transition are easier to read together. Use a hook when one concern can
be named and composed without hiding who owns it. Both use the same retained
model; see State, identity, and ownership.
Adding, removing, or reordering hooks needs a process restart. An ordinary build reports a reordered or conditional hook instead of silently accepting the new identity.
Next
- Build a task list — controllers, signals, and a computed value in one app, one lesson at a time.
- Show shared state — observe a model that lives outside the widget.
- Examples — the runnable companion apps.