Skip to content

Untracked

By default, every read of a @SolidState field inside a reactive context — a build method, an @SolidEffect body, or an @SolidQuery body — registers a dependency. When the field changes, the surrounding computation re-runs: build re-renders the widget subtree at the read site, an effect re-fires, a query re-executes. That’s how fine-grained reactivity stays automatic.

Occasionally you need to read the current value without subscribing. Solid covers two cases.

Reads inside onPressed, onTap, onChanged, and other named-callback parameters starting with on are automatically untracked. Solid treats those as user-interaction handlers — they fire in response to gestures, not signal changes — so registering a dependency on every read inside them would be wrong.

source/conditional_dialog.dart
class ConditionalDialog extends StatelessWidget {
ConditionalDialog({super.key});
@SolidState()
int counter = 0;
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
if (counter > 10) showDialog<void>(/*...*/); // read is untracked
counter++; // writes are always untracked
},
child: const Icon(Icons.add),
);
}
}

You don’t write anything special — Solid handles it.

For one-off untracked reads outside a callback, append .untracked to the field reference. The most common case is reading a value once for key construction or imperative initialization, without rebuilding on later changes.

source/keyed_container.dart
class KeyedContainer extends StatelessWidget {
KeyedContainer({super.key});
@SolidState()
int counter = 0;
@override
Widget build(BuildContext context) {
return Container(
// ValueKey reads counter once; it does NOT rebuild on counter changes.
key: ValueKey(counter.untracked),
child: const Text('hi'),
);
}
}

If you find yourself sprinkling .untracked everywhere, step back — usually a @SolidState getter (derived state) or a non-reactive plain field is the better tool.

.untracked opts out a single read. Sometimes the write is the problem instead. Inside a @SolidEffect, mutating a collection signal you also read creates a cyclic reaction: a collection’s element-write operators read the signal internally (to diff), so the effect subscribes to the very signal it writes and re-fires forever.

Wrap the write in untracked(() => …) to run it without subscribing the surrounding effect. Read your real dependencies first so they stay tracked, then write inside the callback:

source/history_recorder.dart
class HistoryRecorder extends StatelessWidget {
HistoryRecorder({super.key});
@SolidState()
int counter = 0;
@SolidState()
List<int> history = [];
@SolidEffect()
void recordHistory() {
final c = counter; // tracked — re-runs when counter changes
untracked(() => history = [...history, c]); // write is untracked — no self-trigger
}
@override
Widget build(BuildContext context) {
return Text('history has ${history.length} entries');
}
}

Reads inside the callback still see live values — they just don’t subscribe the surrounding effect, query, or build.

It behaves the same for method calls that mutate signals — for example, marking a channel’s messages read while depending on that channel’s message list:

source/message_list.dart
@SolidEffect()
void keepChannelRead() {
messagesController.channelMessages[widget.channelId]; // tracked dependency
untracked(() => messagesController.markAllRead(widget.channelId)); // mutation untracked
}