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.
Inside on* callbacks (automatic)
Section titled “Inside on* callbacks (automatic)”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.
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.
Manual opt-out: field.untracked
Section titled “Manual opt-out: field.untracked”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.
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 writes: untracked(() => …)
Section titled “Untracked writes: untracked(() => …)”.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:
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:
@SolidEffect()void keepChannelRead() { messagesController.channelMessages[widget.channelId]; // tracked dependency untracked(() => messagesController.markAllRead(widget.channelId)); // mutation untracked}