Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 1 addition & 169 deletions packages/signals_core/lib/src/core/computed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,175 +172,7 @@ part of 'signals.dart';
class Computed<T> extends signals.Computed<T>
with ReadonlySignalMixin<T>, SignalsAutoDisposeMixin<T>
implements ReadonlySignal<T> {
/// {@template computed}
/// Data is often derived from other pieces of existing data. The `computed` function lets you combine the values of multiple signals into a new signal that can be reacted to, or even used by additional computeds. When the signals accessed from within a computed callback change, the computed callback is re-executed and its new return value becomes the computed signal's value.
///
/// > `Computed` class extends the [`Signal`](/core/signal/) class, so you can use it anywhere you would use a signal.
///
/// ```dart
/// import 'package:signals/signals.dart';
///
/// final name = signal("Jane");
/// final surname = signal("Doe");
///
/// final fullName = computed(() => name.value + " " + surname.value);
///
/// // Logs: "Jane Doe"
/// print(fullName.value);
///
/// // Updates flow through computed, but only if someone
/// // subscribes to it. More on that later.
/// name.value = "John";
/// // Logs: "John Doe"
/// print(fullName.value);
/// ```
///
/// Any signal that is accessed inside the `computed`'s callback function will be automatically subscribed to and tracked as a dependency of the computed signal.
///
/// > Computed signals are both lazily evaluated and memoized
///
/// ## Force Re-evaluation
///
/// You can force a computed signal to re-evaluate by calling its `.recompute` method. This will re-run the computed callback and update the computed signal's value.
///
/// ```dart
/// final name = signal("Jane");
/// final surname = signal("Doe");
/// final fullName = computed(() => name.value + " " + surname.value);
///
/// fullName.recompute(); // Re-runs the computed callback
/// ```
///
/// ## Disposing
///
/// ### Auto Dispose
///
/// If a computed signal is created with autoDispose set to true, it will automatically dispose itself when there are no more listeners.
///
/// ```dart
/// final s = computed(() => 0, autoDispose: true);
/// s.onDispose(() => print('Signal destroyed'));
/// final dispose = s.subscribe((_) {});
/// dispose();
/// final value = s.value; // 0
/// // prints: Signal destroyed
/// ```
///
/// A auto disposing signal does not require its dependencies to be auto disposing. When it is disposed it will freeze its value and stop tracking its dependencies.
///
/// This means that it will no longer react to changes in its dependencies.
///
/// ```dart
/// final s = computed(() => 0);
/// s.dispose();
/// final value = s.value; // 0
/// final b = computed(() => s.value); // 0
/// // b will not react to changes in s
/// ```
///
/// You can check if a signal is disposed by calling the `.disposed` method.
///
/// ```dart
/// final s = computed(() => 0);
/// print(s.disposed); // false
/// s.dispose();
/// print(s.disposed); // true
/// ```
///
/// ### On Dispose Callback
///
/// You can attach a callback to a signal that will be called when the signal is destroyed.
///
/// ```dart
/// final s = computed(() => 0);
/// s.onDispose(() => print('Signal destroyed'));
/// s.dispose();
/// ```
///
///
/// ## Flutter
///
/// In Flutter if you want to create a signal that automatically disposes itself when the widget is removed from the widget tree and rebuilds the widget when the signal changes, you can use the `createComputed` inside a stateful widget.
///
/// ```dart
/// import 'package:flutter/material.dart';
/// import 'package:signals/signals_flutter.dart';
///
/// class CounterWidget extends StatefulWidget {
/// @override
/// _CounterWidgetState createState() => _CounterWidgetState();
/// }
///
/// class _CounterWidgetState extends State<CounterWidget> with SignalsMixin {
/// late final counter = createSignal(0);
/// late final isEven = createComputed(() => counter.value.isEven);
/// late final isOdd = createComputed(() => counter.value.isOdd);
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Center(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: [
/// Text('Counter: even=$isEven, odd=$isOdd'),
/// ElevatedButton(
/// onPressed: () => counter.value++,
/// child: Text('Increment'),
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// No `Watch` widget or extension is needed, the signal will automatically dispose itself when the widget is removed from the widget tree.
///
/// The `SignalsMixin` is a mixin that automatically disposes all signals created in the state when the widget is removed from the widget tree.
///
/// ## Testing
///
/// Testing computed signals is possible by converting a computed to a stream and testing it like any other stream in Dart.
///
/// ```dart
/// test('test as stream', () {
/// final a = signal(0);
/// final s = computed(() => a());
/// final stream = s.toStream();
///
/// a.value = 1;
/// a.value = 2;
/// a.value = 3;
///
/// expect(stream, emitsInOrder([0, 1, 2, 3]));
/// });
/// ```
///
/// `emitsInOrder` is a matcher that will check if the stream emits the values in the correct order which in this case is each value after a signal is updated.
///
/// You can also override the initial value of a computed signal when testing. This is is useful for mocking and testing specific value implementations.
///
/// ```dart
/// test('test with override', () {
/// final a = signal(0);
/// final s = computed(() => a()).overrideWith(-1);
///
/// final stream = s.toStream();
///
/// a.value = 1;
/// a.value = 2;
/// a.value = 2; // check if skipped
/// a.value = 3;
///
/// expect(stream, emitsInOrder([-1, 1, 2, 3]));
/// });
/// ```
///
/// `overrideWith` returns a new computed signal with the same global id sets the value as if the computed callback returned it.
/// @link https://dartsignals.dev/core/computed
/// {@endtemplate}
/// {@macro computed}
Computed(
super.fn, {
this.debugLabel,
Expand Down
98 changes: 1 addition & 97 deletions packages/signals_core/lib/src/core/effect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,103 +109,7 @@ class Effect extends signals.Effect {
/// Label used for debugging
final String? debugLabel;

/// {@template effect}
/// 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)`](/core/computed). By default all updates are lazy, so nothing will update until you access a signal inside `effect`.
///
/// ```dart
/// 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.
///
/// ```dart
/// 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.
///
/// ```dart
/// 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.
///
/// ```dart
/// 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)`](/core/untracked) to read a signal without subscribing to it.
///
/// ```dart
/// 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;
/// }
/// ```
/// @link https://dartsignals.dev/core/effect
/// {@endtemplate}
/// {@macro effect}
Effect(
super.fn, {
this.debugLabel,
Expand Down
9 changes: 1 addition & 8 deletions packages/signals_core/lib/src/core/signal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@ part of 'signals.dart';
class Signal<T> extends signals.Signal<T>
with ReadonlySignalMixin<T>, SignalsAutoDisposeMixin<T>
implements ReadonlySignal<T> {
/// Simple writeable signal.
///
/// ```dart
/// final count = signal(0);
/// print(count.value); // 0
/// count.value++;
/// print(count.value); // 1
/// ```
/// {@macro signal}
Signal(
super.internalValue, {
this.debugLabel,
Expand Down