Skip to content

Commit

Permalink
Merge pull request #225 from rodydavis/5.0.0
Browse files Browse the repository at this point in the history
v5
  • Loading branch information
rodydavis committed Apr 13, 2024
2 parents 85c9d71 + a09a832 commit 4d744bc
Show file tree
Hide file tree
Showing 333 changed files with 12,267 additions and 145,895 deletions.
233 changes: 11 additions & 222 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,235 +8,24 @@

# Signals.dart

Complete dart port of [Preact signals](https://preactjs.com/blog/introducing-signals/) and takes full advantage of [signal boosting](https://preactjs.com/blog/signal-boosting/).
## Features

Documentation Site: https://dartsignals.dev/
- 🪡 **Fine grained reactivity**: Based on [Preact Signals](https://preactjs.com/blog/signal-boosting/) and provides a fine grained reactivity system that will automatically track dependencies and free them when no longer needed
- ⛓️ **Lazy evaluation**: Signals are lazy and will only compute values when read. If a signal is not read, it will not be computed
- 🗜️ **Flexible API**: Every app is different and signals can be composed in multiple ways. There are a few rules to follow but the API surface is small
- 🔬 **Surgical Rendering**: Widgets can be rebuilt surgically, only marking dirty the parts of the Widget tree that need to be updated and if mounted
- 💙 **100% Dart Native**: Supports Dart JS (HTML), Shelf Server, CLI (and Native), VM, Flutter (Web, Mobile and Desktop). Signals can be used in any Dart project

To start using signals, check out the full [documentation](https://dartsignals.dev/).

VS Code Extension: https://marketplace.visualstudio.com/items?itemName=rodydavis.signals-dart

## Packages

| Package | Pub |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| [`signals`](packages/signals) | [![signals](https://img.shields.io/pub/v/signals.svg)](https://pub.dev/packages/signals) |
| [`signals_core`](packages/signals_core) | [![signals_core](https://img.shields.io/pub/v/signals_core.svg)](https://pub.dev/packages/signals_core) |
| [`signals_flutter`](packages/signals_flutter) | [![signals_flutter](https://img.shields.io/pub/v/signals_flutter.svg)](https://pub.dev/packages/signals_flutter) |
| [`signals_lint`](packages/signals_lint) | [![signals_lint](https://img.shields.io/pub/v/signals_lint.svg)](https://pub.dev/packages/signals_lint) |
| [`signals_devtools_extension`](packages/signals_devtools_extension) | |

## Guide / API

The signals library exposes four functions which are the building blocks to model any business logic you can think of.

```mermaid
graph LR;
Signal-->Computed;
Computed-->Computed;
Computed-->Effect;
Signal-->Effect;
```

### `signal(initialValue)`

The `signal` function creates a new signal. A signal is a container for a value that can change over time. You can read a signal's value or subscribe to value updates by accessing its `.value` property.

```dart
import 'package:signals/signals.dart';
final counter = signal(0);
// Read value from signal, logs: 0
print(counter.value);
// Write to a signal
counter.value = 1;
```

Writing to a signal is done by setting its `.value` property. Changing a signal's value synchronously updates every [computed](#computedfn) and [effect](#effectfn) that depends on that signal, ensuring your app state is always consistent.

#### `signal.peek()`

In the rare instance that you have an effect that should write to another signal based on the previous value, but you _don't_ want the effect to be subscribed to that signal, you can read a signals's previous value via `signal.peek()`.

```dart
final counter = signal(0);
final effectCount = signal(0);
effect(() {
print(counter.value);
// Whenever this effect is triggered, increase `effectCount`.
// But we don't want this signal to react to `effectCount`
effectCount.value = effectCount.peek() + 1;
});
```

Note that you should only use `signal.peek()` if you really need it. Reading a signal's value via `signal.value` is the preferred way in most scenarios.

### `untracked(fn)`

In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use `untracked` to prevent any subscriptions from happening.

```dart
final counter = signal(0);
final effectCount = signal(0);
final fn = () => effectCount.value + 1;
effect(() {
print(counter.value);
// Whenever this effect is triggered, run `fn` that gives new value
effectCount.value = untracked(fn);
});
```

### `computed(fn)`

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.

```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.

### `effect(fn)`

The `effect` function is the last piece that makes everything reactive. When you access a signal inside an `effect`'s 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)`](#computedfn). 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";
```

#### Warning 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)`](#untrackedfn) 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;
}
```

### `batch(fn)`

The `batch` function allows you to combine multiple signal writes into one single update that is triggered at the end when the callback completes.

```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));
// Combines both signal writes into one update. Once the callback
// returns the `effect` will trigger and we'll log "Foo Bar"
batch(() {
name.value = "Foo";
surname.value = "Bar";
});
```

When you access a signal that you wrote to earlier inside the callback, or access a computed signal that was invalidated by another signal, we'll only update the necessary dependencies to get the current value for the signal you read from. All other invalidated signals will update at the end of the callback function.

```dart
import 'package:signals/signals.dart';
final counter = signal(0);
final _double = computed(() => counter.value * 2);
final _triple = computed(() => counter.value * 3);
effect(() => print(_double.value, _triple.value));
batch(() {
counter.value = 1;
// Logs: 2, despite being inside batch, but `triple`
// will only update once the callback is complete
print(_double.value);
});
// Now we reached the end of the batch and call the effect
```

Batches can be nested and updates will be flushed when the outermost batch call completes.

```dart
import 'package:signals/signals.dart';
final counter = signal(0);
effect(() => print(counter.value));
batch(() {
batch(() {
// Signal is invalidated, but update is not flushed because
// we're still inside another batch
counter.value = 1;
});
// Still not updated...
});
// Now the callback completed and we'll trigger the effect.
```

## DevTools

![](packages/signals/doc/screenshots/graph.png)
![](packages/signals/doc/screenshots/list.png)
43 changes: 43 additions & 0 deletions benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
45 changes: 45 additions & 0 deletions benchmark/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "abb292a07e20d696c4568099f918f6c5f330e6b0"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: android
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: ios
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: linux
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: macos
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: web
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
- platform: windows
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
19 changes: 19 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Signals Benchmark

## Results
Date: 2024-04-07T22:40:05.756143

| name | total runs | average run | runs/second | units/second | time per unit | total time | units |
|-----------------------------------------|------------|-------------|-------------|--------------|---------------|------------|-------|
| signals: signal => 0 subscribers | 12393 | 161 μs | 6211.18 | 62111801.24 | 0.0161 μs | 2.0001 s | 10000 |
| signals: signal => 1 subscribers | 3990 | 501 μs | 1996.01 | 19960079.84 | 0.0501 μs | 2.0000 s | 10000 |
| signals: signal => 10 subscribers | 547 | 3.6580 ms | 273.37 | 2733734.28 | 0.3658 μs | 2.0011 s | 10000 |
| signals: signal => 100 subscribers | 48 | 41.976 ms | 23.82 | 238231.37 | 4.1976 μs | 2.0149 s | 10000 |
| solidart: signal => 0 subscribers | 2403 | 832 μs | 1201.92 | 12019230.77 | 0.0832 μs | 2.0005 s | 10000 |
| solidart: signal => 1 subscribers | 369 | 5.4290 ms | 184.20 | 1841959.85 | 0.5429 μs | 2.0035 s | 10000 |
| solidart: signal => 10 subscribers | 34 | 59.453 ms | 16.82 | 168200.09 | 5.9453 μs | 2.0214 s | 10000 |
| solidart: signal => 100 subscribers | 4 | 590.43 ms | 1.69 | 16936.87 | 59.043 μs | 2.3617 s | 10000 |
| state_beacon: signal => 0 subscribers | 14001 | 142 μs | 7042.25 | 70422535.21 | 0.0142 μs | 2.0000 s | 10000 |
| state_beacon: signal => 1 subscribers | 6319 | 316 μs | 3164.56 | 31645569.62 | 0.0316 μs | 2.0004 s | 10000 |
| state_beacon: signal => 10 subscribers | 642 | 3.1160 ms | 320.92 | 3209242.62 | 0.3116 μs | 2.0007 s | 10000 |
| state_beacon: signal => 100 subscribers | 94 | 21.426 ms | 46.67 | 466722.67 | 2.1426 μs | 2.0141 s | 10000 |
28 changes: 28 additions & 0 deletions benchmark/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
13 changes: 13 additions & 0 deletions benchmark/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
Loading

0 comments on commit 4d744bc

Please sign in to comment.