Skip to Content
Show shared state

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

NeedUseWho disposes
Own and observe local reactive stateuseSignalThe hook
Own and observe a derived valueuseComputedThe hook
Observe a source somebody else ownsuseSignalValueThe creator of the source
Rebuild only one subtreeSignalValueBuilderThe creator of the source
Run a reactive side effect with cleanupuseSignalEffectThe 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

SymptomCause and fix
The model changes, but the widget never repaintsThe widget read .value without observing. Wrap it in useSignalValue or SignalValueBuilder.
A list assignment changes nothingThe list changed in place. Assign a newly built list to the signal instead.
The value updates once, then stopsThe read happened in a callback or a deferred builder. Observe it with useSignalValue or SignalValueBuilder.
The app throws while an effect runsAn 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 itHand 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.

Last updated on