Skip to content

Commit

Permalink
Updated README for 0.5
Browse files Browse the repository at this point in the history
  • Loading branch information
mstniy committed Sep 28, 2024
1 parent fb4e0cc commit e35ae21
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 64 deletions.
129 changes: 85 additions & 44 deletions packages/computed/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
A straightforward, reliable, performant and testable reactive state management solution for Dart.
A performant and flexible reactive programming framework for Dart.

> [!TIP]
> Are you using Flutter? Then make sure to check out [Computed Flutter](https://github.com/mstniy/computed_flutter).
---

Computed makes it a breeze to write code that reacts to external events. Just define how you compute your app state based on external data sources, and what effects you want as a result, and Computed will take care of everything else, allowing you to define your app state declaratively, rather than imperatively.
Computed makes it a breeze to express state that stays coherent in response to external events. Just define how you compute your state based on external data sources, and what effects you want as a result, and Computed will take care of everything else, allowing you to handle reactivity declaratively, rather than imperatively.

Computed:

Expand All @@ -16,24 +16,35 @@ Computed:
- Memoizes computation results
- Defers computations as much as possible
- Runs the computations in a topologically-consistent order upon external events.
- Allows you to reliably test your app's logic with no boilerplate.
- Avoids re-running computations unless their upstream change.

## <a name='table-of-contents'></a>Table of contents

<!-- vscode-markdown-toc -->

- [Table of contents](#table-of-contents)
- [Here is how it works](#here-is-how-it-works)
- [A larger example](#a-larger-example)
- [Computation Configs](#computation-configs)
- [`memoized`](#`memoized`)
- [`assertIdempotent`](#`assertidempotent`)
- [`async`](#`async`)
- [`dispose`](#`dispose`)
- [`onCancel`](#`oncancel`)
- [Effects](#effects)
- [Testing](#testing)
- [Looking at the past](#looking-at-the-past)
- [Computed queries](#computed-queries)
- [Stream utilities](#stream-utilities)
- [`ComputationCache`](#`computationcache`)
- [Advanced usage](#advanced-usage)
- [Using computations opportunistically](#using-computations-opportunistically)
- [Customizing downstream](#customizing-downstream)
- [FAQ](#faq)
- [Pitfalls](#pitfalls)
- [Do not use mutable values in computations](#do-not-use-mutable-values-in-computations)
- [Use the async mode for computations kicking off async operations](#use-the-async-mode-for-computations-kicking-off-async-operations)
- [Do not `.use` a `Future`/`Stream` inside the computation that created it](#do-not-`.use`-a-`future`/`stream`-inside-the-computation-that-created-it)
- [Do not `.use` `Future`s or `Stream`s inside the computation that created it](#do-not-`.use`-`future`s-or-`stream`s-inside-the-computation-that-created-it)
- [Do not create computations inside computations](#do-not-create-computations-inside-computations)
- [`Future`s returned from computations are not awaited](#`future`s-returned-from-computations-are-not-awaited)
- [Do not forget to `.use` or `.react` your data sources](#do-not-forget-to-`.use`-or-`.react`-your-data-sources)
- [Keep in mind that `.prev` does not subscribe](#keep-in-mind-that-`.prev`-does-not-subscribe)
Expand Down Expand Up @@ -65,10 +76,10 @@ Here is how you can do this using Computed:

```
final sub = $(() => s.use * 2).
asStream.listen(db.write);
listen(db.write);
```

That's it. Computed will take care of re-running the computation and calling the listener as needed. Note how you did not need to specify a dependency list for the computation, Computed discovers it automatically. You don't even have any mutable state in your app code.
That's it. Computed will take care of re-running the computation and calling the listener as needed. Note how you did not need to specify a dependency list for the computation, Computed discovers it automatically. You don't even have any explicit mutable state in your code.

To cancel the listener, you can use `.cancel()`:

Expand Down Expand Up @@ -96,7 +107,7 @@ And the other is a stream of lists of integers:
Stream<List<int>> items;
```

Assume for the sake of example that your business logic requires you to filter the items in the second stream by the threshold specified by the first stream, and save the results to a database.
Assume for the sake of example that you want to implement some logic requiring you to filter the items in the second stream by the threshold specified by the first stream, and save the results to a database.

Here is how you might approach this problem using an imperative approach.

Expand Down Expand Up @@ -136,61 +147,67 @@ void updateDB() {
And here is how to do the same using Computed:

```
$(() {
Computed(() {
final currentThreshold = threshold.use;
return items.use.
filter((x) => x > currentThreshold).
toList();
}).asStream.listen(db.write);
}, assertIdempotent: false).listen(db.write);
```

## <a name='effects'></a>Effects
Note that use of [`assertIdempotent: false`](#assertidempotent), as `List` does not have value semantics around its equality operator. In general, [`computed_collections`](https://github.com/mstniy/computed.dart/tree/master/packages/computed_collections) is the better solution for expressing reactive collections.

Effects allow you to define computations with side effects. Like `.listen`, effects ultimately trigger the computation graph for the computations they use.
Effects are particularly useful if you wish to define side effects depending on multiple data sources or computations:
## <a name='computation-configs'></a>Computation Configs

```
Stream<PageType> activePage;
Stream<bool> isDarkMode;
The shorthand `$` notation used in the previous examples is a quick way of expressing "pure" reactive computations, computations which are idempotent with respect to the equality operator of the result and have no side effects. Computed also allows you to express computations which are not idempotent and/or have side effects using the following configuration parameters:

final sub = Computed.effect(() => sendAnalytics(activePage.use, isDarkMode.use));
```
### <a name='`memoized`'></a>`memoized`

Like listeners effects can be cancelled with `.cancel()`:
If set to `true` (default), Computed "memoizes" the value of this computation, skipping to notify the downstream/listeners if this computation returns a value or throws an exception comparing equal to the previous one.

```
sub.cancel();
```
### <a name='`assertidempotent`'></a>`assertIdempotent`

## <a name='testing'></a>Testing
If set to `true` (default), Computed runs this computation twice initially and in response to upstream changes in debug mode and checks if the two runs return equal values or throw equal exceptions. If not, it asserts.

Computed lets you mock any computation or data source, allowing you to easily test downstream behaviour.
### <a name='`async`'></a>`async`

Here is how you could test a situation where the threshold was a set value in the previous example:
If set to `false` (default), Computed runs this computation in a [Zone](https://dart.dev/libraries/async/zones) that disallows async operations. Setting this to `true` implies `assertIdempotent==false`.

```
threshold.mockEmit(42); // Downstream behaves as if 42 was emitted
```
### <a name='`dispose`'></a>`dispose`

If set, this callback is called with the last value returned by the computation when it:

- changes value,
- switches from producing values to throwing exceptions or
- loses all of its listeners and non-weak downstream computations,

if it previously had a value.

### <a name='`oncancel`'></a>`onCancel`

If set, this callback be called when the computation loses all of its listeners and non-weak downstream computations. Called after `dispose`.

## <a name='effects'></a>Effects

You can also mock computations:
Effects allow you to define computations with side effects. Like `.listen`, effects ultimately trigger the computation graph for the computations they use.
Effects are particularly useful if you wish to define side effects depending on multiple data sources or computations:

```
c.fix(42); // Will always return 42
c.fixThrow(FooException()); // Will always throw FooException
c.mock(() => dataSource.use); // Can mock to arbitrary computations
Stream<PageType> activePage;
Stream<bool> isDarkMode;
final sub = Computed.effect(() => sendAnalytics(activePage.use, isDarkMode.use));
```

You can undo the mock at any time to return to the original computation:
Like listeners effects can be cancelled with `.cancel()`:

```
c.unmock();
sub.cancel();
```

These actions will trigger a re-computation if necessary.

## <a name='looking-at-the-past'></a>Looking at the past

`.use` returns the current value of the data source or computation, so how can you look at the past without resorting to keeping mutable state in your app code? `.prev` allows you to obtain the last value assumed by a given data source or computation the last time the current computation changed its value or otherwise notified its listeners or other computations which depend on it.
`.use` returns the current value of the data source or computation, so how can you look at the past without resorting to keeping mutable state in your app code? `.prev` allows you to obtain the last value assumed by a given data source or computation the last time the current computation ran.

Here is a simple example that computes the difference between the old and new values of a data source whenever it produces a value:

Expand All @@ -206,7 +223,7 @@ final c = $(() {
Note the use of `.react` in this example.
`.react` marks the current computation to be recomputed for all values produced by a data source, even if it consecutively produces a pair of values comparing `==`. `.react` will run the given function if the data source has produced a new value/error. As a rule of thumb, you should use `.react` over `.use` for data sources representing a sequence of events rather than a state.
`.prevOr` is a handy shortcut which returns the given fallback value instead of throwing `NoValueException` if the data source had no value the last time the current computation notified its listeners or other computations which depend on it.
`memoized: false` prevents the result of the computation from being memoized, as we want the result of `.prevOr` to always reflect the last value of `s`, even if the result of the computation did not change.
[`memoized: false`](#memoized) prevents the result of the computation from being memoized, as we want the downstream computations and listeners to be notificed even if the difference did not change.

You can also create temporal accumulators:

Expand Down Expand Up @@ -242,12 +259,32 @@ Of course, other computations can use the result of the computed query, as it is

## <a name='stream-utilities'></a>Stream utilities

Computed includes a minimal set of `Stream`s likely to be useful in a reactive setting. You can find them at [packages/computed/lib/utils](packages/computed/lib/utils).
Computed includes a minimal set of `Stream`s likely to be useful in a reactive setting. You can find them at [lib/utils](lib/utils).

## <a name='`computationcache`'></a>`ComputationCache`

You can find yourself in a situtation where you want to create parameterized computations. The naive way is to create a new computation each time, but this results in duplicated computation if multiple computations with the same parameters are created. For such cases, you can use `ComputationCache` to "unify" computations with identical parameters. `ComputationCache` makes sure each cache key has at most one active computation at any point in time, no matter how many computations for that key have been created. Of course, it preserves the reactive semantics by notifying the listeners and downstream computations of all the created computations whenever necessary.

## <a name='advanced-usage'></a>Advanced usage

This section describes advanced capabilities of Computed. You should seldom need them.

### <a name='using-computations-opportunistically'></a>Using computations opportunistically

You may find yourself in a situation where there are multiple ways to compute a value, where one way is expensive but computes several related values, and the other is computationally cheaper but more focused. In such cases, you can use `useWeak`. It allows computations to reactively use other computations without triggering them to be computed if they have no listeners or non-weak downstream computations.

Calling `.useWeak` on a computation weakly subscribes the current computation to it. If the target computation has listeners or non-weak downstream, `useWeak` will return its value. Otherwise, it will throw a `NoStrongUserException`. It will schedule the current computation to be re-run if the target computation changes value or loses all of its listeners or non-weak downstream, but not if it gains either.

### <a name='customizing-downstream'></a>Customizing downstream

By default, Computed schedules downstream computations to be re-run whenever a computation changes value. If this is not computationally efficient, you can customize the downstream which should be schedule for being rerun. You can find an example in the [`computed_collections` repo](https://github.com/mstniy/computed.dart/blob/master/packages/computed_collections/lib/src/utils/custom_downstream.dart).

Note that using this feature requires you to depend on the Computed internals, restricting you to individual patch releases. Use it with great care, as Computed has no guardrails against breaking reactive consistency if you use custom downstreams.

## <a name='faq'></a>FAQ

- Q: Why am I getting `Computed expressions must be purely functional. Please use listeners for side effects.`
- A: On debug mode, Computed runs the given computations twice and checks if both calls return the same value. If this does not hold, it throws this assertion. Possible reasons include mutating and using a mutable value inside the computation or returning a type which does not implement deep comparisons, like `List` or `Set`.
- A: On debug mode, Computed runs the given computations twice and checks if both calls return the same value. If this does not hold, it throws this assertion. Possible reasons include mutating and using a mutable value inside the computation or returning a type which does not implement deep comparisons, like `List` or `Set`. Consider returning a type which does implement value semantics for its equality operator, or using [`assertIdempotent: false`](#`assertidempotent`). If your computation kickstarts an async process and returns a `Stream` or `Future`, consider using `Computed.async` as described in [computed queries](#computed-queries).

## <a name='pitfalls'></a>Pitfalls

Expand All @@ -268,13 +305,17 @@ As this may cause Computed to stop tracking `value`, breaking the reactivity of

This will disable some checks which don't make sense for such computations.

### <a name='do-not-`.use`-a-`future`/`stream`-inside-the-computation-that-created-it'></a>Do not `.use` a `Future`/`Stream` inside the computation that created it
### <a name='do-not-`.use`-`future`s-or-`stream`s-inside-the-computation-that-created-it'></a>Do not `.use` a `Future`s or `Stream` inside the computation that created it

As this might lead to an infinite loop between the computation running, creating a new data source, and running again when it produces a value.

### <a name='do-not-create-computations-inside-computations'></a>Do not `.use` a computation inside the computation that created it

As this will lead to an infinite loop.
As this might be inefficient if the outer computation re-creates the inner computation each time. Instead, consider creating the inner computation before the outer computation, and capturing it as part of its closure. If you require reactive values to construct the inner computation, consider constructing it in a dedicated middle computation with `assertIdempotent: false` and `.unwrap`ing the middle computation inside the outer one.

### <a name='`future`s-returned-from-computations-are-not-awaited'></a>`Future`s returned from computations are not awaited

At least for memoization purposes. The re-run pass never awaits, even if a computation returns a `Future`. They will be passed as-is to downstream computations. If you want to instead return the values produced by the asynchronous operations returned by a computation, see `.unwrap`.
At least for memoization purposes. The DAG pass never awaits, even if a computation returns a `Future`. The results are passed as-is to downstream computations. If you want to instead return the values produced by the asynchronous operations returned by a computation, see `.unwrap`.

### <a name='do-not-forget-to-`.use`-or-`.react`-your-data-sources'></a>Do not forget to `.use` or `.react` your data sources

Expand All @@ -287,5 +328,5 @@ It is also "subjective" to the running computation. Different functions can have
### <a name='`.react`-is-not-`.listen`'></a>`.react` is not `.listen`

`.react` can only be used inside computations, `.listen` can only be used outside computations.
`.react` callbacks must not have side effects. `.listen` callbacks are where side effects are supposed to happen.
`.react` callbacks must not have side effects beyond the local scope of the current computation. `.listen` callbacks are where side effects are supposed to happen.
`.listen` keeps a reference to the given callback and calls it at a later point in time. `.react` either calls the given function before returning or doesn't call it at all.
22 changes: 2 additions & 20 deletions packages/computed/lib/utils/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
This folder hosts a minimal set of `Stream`s which are likely to be useful in a reactive setting.

Note that these classes are not exclusive to Computed, as they are Dart `Stream`s after all. They are, however, designed with Computed in mind, and consuming them from inside a computation merely requires a `.use`, as with any other `Stream`.
Note that these classes are not exclusively available to Computed, as they are Dart `Stream`s after all. They are, however, designed with Computed in mind. Consuming them from inside a computation merely requires a `.use`, as with any other `Stream`.

### 1. <a name='`valuestream`'></a>`ValueStream`
### `ValueStream`

A `Stream` with last value semantics, similar to Flutter's `ValueNotifier` or rxdart's `BehaviorSubject`:

Expand All @@ -20,21 +20,3 @@ final s = ValueStream.seeded(0);
```

Note that `ValueStream` does not provide a way to get the last value synchronously. This is intentional, as this would be an easy way to break your application's reactivity.

### 2. <a name='`resourcestream`'></a>`ResourceStream`

A `ValueStream`, where the values are resources created by a given function, and need to be disposed of by another given function:

```
final s = ResourceStream<FictionalDatabase>(
() => FictionalDatabase(),
(db) => db.dispose()
);
final sub = s.listen(...); // Creates a `FictionalDatabase`
s.add(FictionalDatabase()); // Disposes of the first one
sub.cancel(); // Disposes of the second one
final c = $(() => s.use.getVersion());
c.listen(...); // Creates a new `FictionalDatabase`
```

0 comments on commit e35ae21

Please sign in to comment.