LogoSignals.dart
Copy Markdown
rodydavis/signals.dart 999999

Type: Effect

API reference and details for Effect from signals.dart.

Effect#

Kind: class & function  |  Package: package:signals_core

Class: Effect#

An Effect is the passive consumer of the reactive model. It allows you to run arbitrary side-effects (such as logging, database writes, or UI rendering updates) whenever any of its tracked dependencies mutate.

Any signal accessed via .value inside the effect callback is automatically registered as a dependency. When a dependency changes, the effect is re-run.

Core Example#

import 'package:signals/signals.dart';

final counter = signal(0);

// Create an active effect: prints "Count: 0" immediately
final dispose = effect(() {
  print('Count: ${counter.value}');
});

// Updating the signal re-runs the effect: prints "Count: 1"
counter.value = 1;

// Clean up the effect to unsubscribe from updates
dispose();

Key API Capabilities#

1. Lifecycle Cleanup Callback#

You can return an optional void Function() from the effect callback block. This cleanup function is automatically invoked the next time the effect is re-run, or when the effect is permanently disposed. This is perfect for cancelling timers, sockets, or other active event channels.

final counter = signal(0);

effect(() {
  final count = counter.value;
  final timer = Timer(Duration(seconds: 1), () => print('Timer fired for $count'));

  // Cleanup function called before re-run or dispose
  return () => timer.cancel();
});

2. Lifecycle Listeners via onDispose#

You can attach explicit cleanup routines that run when the effect is destroyed by passing onDispose in the options or by calling .onDispose(callback) directly on the effect instance.

final counter = signal(0);
final dispose = effect(
  () => print(counter.value),
  options: EffectOptions(
    onDispose: () => print('Effect has been unmounted!'),
  ),
);

dispose(); // Prints: "Effect has been unmounted!"

⚠️ Warning: Preventing Cycles#

Mutating a signal directly inside an effect that reads that same signal will cause an infinite loop (cycle), throwing a StackOverflowError or a cycle exception. To read a signal inside an effect without subscribing to its updates, wrap the read operation in untracked().

final a = signal(0);
final b = signal(0);

effect(() {
  // Active subscription to a
  final valA = a.value;

  // Read b non-reactively using untracked to prevent circular subscriptions
  final valB = untracked(() => b.value);

  print('A: $valA, B: $valB');
});

Members of Effect#

Member Type Signature Description
debugLabel method dart String? debugLabel Label used for debugging
Effect constructor dart Effect(super.fn, {EffectOptions? options, @Deprecated('Use options: EffectOptions(autoDispose: ...) instead') bool? autoDispose, @Deprecated('Use options: EffectOptions(name: ...) instead') String? debugLabel, @Deprecated('Use options: EffectOptions(onDispose: ...) instead') void Function()? onDispose}) The effect function is the last piece that makes everything reactive. When you access a signal inside its callback function, that signal and every dependency of said signal will be activated and subscribed to. In that regard it is very similar to computed(fn) . By default all updates are lazy, so nothing will update until you access a signal inside effect .
call method dart void Function() call()
dispose method dart void dispose()
disposed method dart bool disposed Check if the effect is disposed
disposed method dart disposed(bool value) Force an effect to be disposed
onDispose method dart EffectCleanup onDispose(void Function() cleanup) Add a cleanup function to be called when the signal is disposed

Class: Effect#

Represents a passive observer that runs arbitrary side-effect code in response to signal changes.

An Effect tracks which signals are accessed within its callback function, and automatically schedules itself to re-run whenever those dependencies change.

Under the hood, the reactivity engine tracks reads on .value inside the active effect block. Once the block completes, a subscription is registered for each accessed signal. When any of those signals mutate, the effect is added to the microtask queue and executed synchronously during the next tick.

Do not modify a tracked signal directly inside an effect callback, as this will trigger another execution of the same effect, causing an infinite loop (cycle) and throwing a cycle detection error. To read a signal non-reactively, use .peek().

Example Usage#

1. Standard Side-Effect

import 'package:preact_signals/preact_signals.dart';

final count = signal(0);

void main() {
  // Creates and immediately starts the effect
  final logger = Effect(() {
    print('Active count is: ${count.value}');
  });

  count.value = 1; // Prints: "Active count is: 1"
  logger.dispose();
}

2. Effect Cleanup Callback

If your effect returns a function, that function is registered as a cleanup callback. The cleanup callback is executed right before the next effect run, or when the effect is disposed. This is highly useful for cleaning up timers, controllers, or other subscriptions:

final query = signal('search_term');

final searchEffect = Effect(() {
  final currentQuery = query.value;
  print('Initiating search for: $currentQuery');

  final timer = Timer(Duration(milliseconds: 500), () {
    print('Search completed for: $currentQuery');
  });

  // Return cleanup callback
  return () {
    print('Cancelling previous search timer');
    timer.cancel();
  };
});

Members of Effect#

Member Type Signature Description
globalId field dart int globalId
flags field dart int flags
name field dart String? name The name of the effect for debugging.
Effect constructor dart Effect(this.fn, {String? name, EffectOptions? options}) Creates a new Effect instance with the passive side-effect callback fn .
notify method dart void notify()
dispose method dart void dispose() Disposes of the effect, stopping future callback executions,
call method dart void Function() call() Activates/Runs the effect immediately.

Function: effect#

EffectCleanup effect(EffectCallback fn, {EffectOptions? options, @Deprecated('Use options: EffectOptions(autoDispose: ...) instead') bool? autoDispose, @Deprecated('Use options: EffectOptions(name: ...) instead') String? debugLabel, @Deprecated('Use options: EffectOptions(onDispose: ...) instead') void Function()? onDispose})

The effect function is the last piece that makes everything reactive. When you access a signal inside its callback function, that signal and every dependency of said signal will be activated and subscribed to. In that regard it is very similar to computed(fn). By default all updates are lazy, so nothing will update until you access a signal inside effect.

import 'package:signals/signals.dart';

final name = signal("Jane");
final surname = signal("Doe");
final fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
effect(() => print(fullName.value));

// Updating one of its dependencies will automatically trigger
// the effect above, and will print "John Doe" to the console.
name.value = "John";

You can destroy an effect and unsubscribe from all signals it was subscribed to, by calling the returned function.

import 'package:signals/signals.dart';

final name = signal("Jane");
final surname = signal("Doe");
final fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
final dispose = effect(() => print(fullName.value));

// Destroy effect and subscriptions
dispose();

// Update does nothing, because no one is subscribed anymore.
// Even the computed `fullName` signal won't change, because it knows
// that no one listens to it.
surname.value = "Doe 2";

Cleanup Callback#

You can also return a cleanup function from an effect. This function will be called when the effect is destroyed.

import 'package:signals/signals.dart';

final s = signal(0);

final dispose = effect(() {
  print(s.value);
  return () => print('Effect destroyed');
});

// Destroy effect and subscriptions
dispose();

On Dispose Callback#

You can also pass a callback to effect that will be called when the effect is destroyed.

import 'package:signals/signals.dart';

final s = signal(0);

final dispose = effect(() {
  print(s.value);
}, onDispose: () => print('Effect destroyed'));

// Destroy effect and subscriptions
dispose();

Warning About Cycles#

Mutating a signal inside an effect will cause an infinite loop, because the effect will be triggered again. To prevent this, you can use untracked(fn) to read a signal without subscribing to it.

import 'dart:async';

import 'package:signals/signals.dart';

Future<void> main() async {
  final completer = Completer<void>();
  final age = signal(0);

  effect(() {
    print('You are ${age.value} years old');
    age.value++; // <-- This will throw a cycle error
  });

  await completer.future;
}

Function: effect#

void Function() effect( Function() fn, [EffectOptions? options])

Creates and immediately executes a new reactive Effect.

Returns a bound disposer function that can be called to stop the effect and unsubscribe it from all tracked signals.

Example Usage#

import 'package:preact_signals/preact_signals.dart';

final count = signal(0);
final dispose = effect(() {
  print('Count is: ${count.value}');
  return () => print('Cleaning up!');
});

void main() {
  count.value = 10; // Prints: "Cleaning up!" then "Count is: 10"
  dispose(); // Stops the effect and unsubscribes
}

References#

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