LogoSignals.dart
Copy Markdown
rodydavis/signals.dart 999999

Type: Signal

API reference and details for Signal from signals.dart.

Signal#

Kind: class & function  |  Package: package:preact_signals

Class: Signal#

Represents a mutable reactive state container that sits at the foundation of the reactivity system.

Signals hold a single, mutable value that can be read or modified. When a signal's value is updated, any active computations (like Computed) or effects (like effect) that read the signal's value inside their execution context are automatically notified and scheduled to re-run.

Under the hood, this establishes a reactive dependency graph where reading a signal registers the reader as a "target", and updating a signal triggers direct, glitch-free propagation to all registered targets.

Accessing .value inside a reactive context (like effect or computed) registers a dependency. Reading a value outside a reactive context behaves like a standard getter without creating a subscription.

Example Usage#

1. Basic Reactive Flow

import 'package:preact_signals/preact_signals.dart';

void main() {
  final count = Signal(0);

  // The effect automatically subscribes to count.value
  effect(() {
    print('Count is: ${count.value}');
  });

  count.value = 5; // Triggers print: Count is: 5
}

2. Controlling Subscriptions via .peek()

If you need to read a signal's value without subscribing to updates, use the .peek() method:

final count = Signal(0);
final threshold = Signal(10);

effect(() {
  // Subscribes to count, but NOT to threshold
  if (count.value >= threshold.peek()) {
    print('Threshold reached!');
  }
});

Members of Signal#

Member Type Signature Description
globalId field dart int globalId
name field dart String? name
watched field dart void Function()? watched
unwatched field dart void Function()? unwatched
equalityCheck method dart SignalEquality equalityCheck Get the active equality check
isInitialized method dart bool isInitialized Check if the value is set and not a lazy signal
internalValue method dart T internalValue
Signal constructor dart Signal(this._internalValue, {String? name, void Function()? watched, void Function()? unwatched, ReadonlySignalOptions ? options, SignalEquality? equality}) Creates a new Signal instance with the given initial value.
Signal.lazy constructor dart Signal.lazy({String? name, void Function()? watched, void Function()? unwatched, ReadonlySignalOptions ? options, SignalEquality? equality}) Creates a new lazy Signal instance that is computed on-demand upon first read.
version field dart int version Version numbers should always be >= 0, because the special value -1 is used
internalRefresh method dart bool internalRefresh()
subscribeToNode method dart void subscribeToNode(Node node)
unsubscribeFromNode method dart void unsubscribeFromNode(Node node)
subscribe method dart void Function() subscribe(void Function(T value) fn)
value method dart T value Gets the current value of the signal.
value method dart value(T val) Sets the current value of the signal.
set method dart bool set(T val, {bool force = false}) Updates the signal's value by method call.

Class: Signal#

Simple writeable signal

Members of Signal#

Member Type Signature Description
Signal constructor dart Signal(super.internalValue, {SignalOptions ? options, @Deprecated('Use options: SignalOptions(autoDispose: ...) instead') bool? autoDispose, @Deprecated('Use options: SignalOptions(name: ...) instead') String? debugLabel}) Simple writeable signal.
Signal.lazy constructor dart Signal.lazy({SignalOptions ? options, @Deprecated('Use options: SignalOptions(autoDispose: ...) instead') bool? autoDispose, @Deprecated('Use options: SignalOptions(name: ...) instead') String? debugLabel}) Lazy signal that can be created with type T that
debugLabel method dart String? debugLabel
equalityCheck method dart signals.SignalEquality equalityCheck Optional method to check if to values are the same
set method dart bool set(T val, {bool force = false})
value method dart T value
readonly method dart ReadonlySignal readonly() Returns a readonly signal
unsubscribeFromNode method dart void unsubscribeFromNode(Node node)
overrideWith method dart Signal overrideWith(T val) Override the current signal with a new value as if it was created with it.

Function: signal#

Signal<T> signal(T value, {SignalOptions<T>? options, @Deprecated('Use options: SignalOptions(autoDispose: ...) instead') bool? autoDispose, @Deprecated('Use options: SignalOptions(name: ...) instead') String? debugLabel})

A Signal is a reactive container for a value that changes over time. It forms the bedrock of the reactive framework, allowing fine-grained, glitch-free propagation of state updates to dependent computeds and effects.

You can read a signal's current state, mutate it to dispatch updates, or subscribe to changes by accessing its .value property inside any active reactive context.

Core Example#

import 'package:signals/signals.dart';

// Create a reactive signal holding an integer
final counter = signal(0);

// Read the value: prints 0
print(counter.value);

// Write to a signal: dispatches updates to all downstreams synchronously
counter.value = 1;

Key API Capabilities#

1. Reading & Writing via .value#

The .value property is the default way to interact with a signal.

  • Inside a Reactive Context: Accessing .value inside a computed block or effect callback automatically registers the signal as a dependency, establishing an active subscription.
  • Outside a Reactive Context: Acts as a standard getter and setter, allowing you to fetch or update the underlying state.

2. Non-reactive Reads via .peek()#

If you need to read a signal's current value without subscribing to its updates inside a reactive context, use the .peek() method. This is invaluable when writing to another signal inside an effect based on the previous state, preventing infinite update loops (cycles).

final counter = signal(0);
final effectTriggerCount = signal(0);

effect(() {
  // Subscribes to changes of `counter`
  final current = counter.value;
  print('Counter updated: $current');

  // Read current count non-reactively and increment.
  // The effect will NOT subscribe to `effectTriggerCount`.
  effectTriggerCount.value = effectTriggerCount.peek() + 1;
});

3. Accessing the Previous State via .previousValue#

Signals automatically cache their immediately preceding value. Accessing .previousValue lets you perform diffing or historic analysis. Like .peek(), reading .previousValue does not establish a reactive dependency.

final username = signal("initial_user");

effect(() {
  print('Current Username: ${username.value}');
  print('Previous Username: ${username.previousValue}');
});

username.value = "new_user";
// Prints:
// Current Username: new_user
// Previous Username: initial_user

4. Force Updates via .set()#

When dealing with mutable data types (e.g., custom class instances, collections), mutating properties directly does not change the instance reference. You can force an update using .set(..., force: true) to skip standard equality checks and notify all downstreams.

final numbers = signal([1, 2, 3]);

// Modify the list in-place and force notify
numbers.value.add(4);
numbers.set(numbers.value, force: true);

Lifecycle & Resource Management#

Auto-Disposal#

If a signal is constructed with autoDispose: true, it will automatically destroy itself when it no longer has active reactive listeners (subscriptions). This prevents memory leaks by freeing resources as soon as they are out of scope.

final s = signal(0, options: SignalOptions(autoDispose: true));

s.onDispose(() => print('Signal has been disposed!'));

// Create active subscriber
final dispose = s.subscribe((_) {});

// Cancel subscription: s has no listeners, so it self-disposes
dispose();
// Prints: "Signal has been disposed!"

You can manually verify the lifecycle state using .disposed, or register custom clean-up routines via .onDispose(callback).


Flutter Integration#

In Flutter applications, manage state and reactivity seamlessly by using SignalWidget (for stateless widgets) or SignalStatefulWidget (for stateful widgets). These widgets establish an implicit reactive context directly at the element layer. Any signal accessed via .value inside the build method is automatically tracked, and the widget automatically rebuilds when they mutate.

Stateless Example with SignalWidget#

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

final counter = signal(0);

class CounterDisplay extends SignalWidget {
  const CounterDisplay({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count: ${counter.value}'),
            ElevatedButton(
              onPressed: () => counter.value++,
              child: const Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

Stateful Example with SignalStatefulWidget#

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

class CounterDisplay extends SignalStatefulWidget {
  const CounterDisplay({super.key});

  @override
  State<CounterDisplay> createState() => _CounterDisplayState();
}

class _CounterDisplayState extends State<CounterDisplay> {
  // Local signal scoped to this widget state:
  final counter = signal(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count: ${counter.value}'),
            ElevatedButton(
              onPressed: () => counter.value++,
              child: const Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

Testing Strategies#

1. Converting to Streams#

You can convert any reactive signal into a standard Dart Stream by calling .toStream(). This is highly beneficial for testing signal value sequences in order using test matchers.

test('emits sequential count updates in order', () async {
  final counter = signal(0);
  final stream = counter.toStream();

  counter.value = 1;
  counter.value = 2;

  await expectLater(stream, emitsInOrder([0, 1, 2]));
});

2. Dependency Injection & Mock Overrides#

Global or lazy signals used across your application can be mocked or overridden during testing via .overrideWith(value). This returns a new signal sharing the same global identifier, helping you mock complex state dependencies seamlessly.

test('mocking global signals', () {
  final apiToken = signal("production_token");

  // Override with test mock token
  apiToken.overrideWith("mock_test_token");

  expect(apiToken.value, "mock_test_token");
});

Function: signal#

FlutterSignal<T> signal(T value, {core.SignalOptions<T>? options, @Deprecated('Use options: SignalOptions(name: ...) instead') String? debugLabel, @Deprecated('Use options: SignalOptions(autoDispose: ...) instead') bool? autoDispose, bool runCallbackOnListen = false})

Creates a mutable, reactive FlutterSignal initialized with the given value.

When the value changes, all registered builders, effects, and ValueNotifier listeners are automatically scheduled to update/rebuild.

Flutter Widget Example#

final count = signal(0);

class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SignalBuilder(
          builder: (context) => Text('Count: ${count.value}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Function: signal#

Signal<T> signal(T value, [SignalOptions<T>? options])

Convenient global constructor for creating a mutable reactive state signal.

Example Usage#

import 'package:preact_signals/preact_signals.dart';

final count = signal(0);
final name = signal('Jane');

References#

The Signal type is referenced and used in the following pages: