Show shared state
Use this guide when the state already exists outside the widget tree: a document, a session, a search model, a service. The widget reads it and rebuilds when it changes. The model keeps its own life.
If the state belongs to one screen, own it there instead. See State, identity, and ownership for that choice, and Build a task list to learn local signals through an exercise.
Keep the model out of the widget
A model is ordinary signals_core
code with no Noir import, so it works in a test with no terminal:
import 'package:signals_core/signals_core.dart';
class FileSearchModel {
FileSearchModel(List<String> files) : _files = signal(List<String>.of(files));
final Signal<List<String>> _files;
final Signal<String> query = signal('');
late final Computed<List<String>> visibleFiles = computed(() {
final term = query.value.toLowerCase();
return List<String>.unmodifiable(
_files.value.where((file) => file.toLowerCase().contains(term)),
);
});
late final Computed<int> visibleCount = computed(
() => visibleFiles.value.length,
);
void dispose() {
visibleCount.dispose();
visibleFiles.dispose();
query.dispose();
_files.dispose();
}
}One owner creates the model and disposes it, deepest derivation first.
Observe it from a widget
Reading model.visibleFiles.value in build gets the current value and
nothing else. It does not subscribe the widget. The file-search screen
observes each derived source with SignalValueBuilder, so the field host
does not rebuild on every filter:
SignalValueBuilder<int>(
signal: model.visibleCount,
builder: (context, count) => Text('$count files'),
),
SignalValueBuilder<List<String>>(
signal: model.visibleFiles,
builder: (context, visible) => Column(
children: [for (final file in visible) Text(file)],
),
),Each builder subscribes only to the supplied signal. Reading a second signal
inside the callback does not create a second subscription. useSignalValue
observes one borrowed source on the host instead.
Write to the model from input callbacks:
TextInput(
controller: controller,
autofocus: true,
placeholder: 'Filter files',
onChanged: (text) => model.query.value = text,
)The complete program is the
file-search example .
It retains the model with useMemoized, registers useOnDispose(model.dispose),
and observes each derived source with SignalValueBuilder.
Match the API to the ownership
| Need | Use | Who disposes |
|---|---|---|
| Own and observe local reactive state | useSignal | The hook |
| Own and observe a derived value | useComputed | The hook |
| Observe a source somebody else owns | useSignalValue | The creator of the source |
| Rebuild only one subtree | SignalValueBuilder | The creator of the source |
| Run a reactive side effect with cleanup | useSignalEffect | The hook, through the returned cleanup |
useSignal and useComputed own what they create, so autoDispose must stay
false; the hooks reject true. useSignalValue and SignalValueBuilder borrow
a source and never dispose it. A child that receives a model from its parent
observes without taking over disposal.
Observation is explicit
There is no automatic whole-build tracking. SignalWidget and SignalBuilder
retain hooks; the host name does not make a plain .value read reactive. Reads
in event handlers, asynchronous callbacks, and deferred child builders do not
create subscriptions either.
Subscription callbacks request rebuilds. Noir schedules the frame and combines repeated requests. Signals owns the reactive dependency graph; Noir owns the widget tree, layout, painting, and native resources.
When a borrowed value does not update the screen
| Symptom | Cause and fix |
|---|---|
| The model changes, but the widget never repaints | The widget read .value without observing. Wrap it in useSignalValue or SignalValueBuilder. |
| A list assignment changes nothing | The list changed in place. Assign a newly built list to the signal instead. |
| The value updates once, then stops | The read happened in a callback or a deferred builder. Observe it with useSignalValue or SignalValueBuilder. |
| The app throws while an effect runs | An effect changed an observed hook value synchronously. Move the write to an input callback or a timer. |
| A child reads a disposed signal after its parent replaced it | Hand the retired source to deferDispose instead of disposing it during the update. |
The Signals contract is the canonical reference for keys, effect timing, replacement, and deferred disposal. Manage state and resources covers the hook rules the signal hooks share.