{"id":20302371,"url":"https://github.com/re-js/mobx-signals","last_synced_at":"2025-03-04T06:42:01.544Z","repository":{"id":177885018,"uuid":"661021797","full_name":"re-js/mobx-signals","owner":"re-js","description":"MobX Signals Implementation","archived":false,"fork":false,"pushed_at":"2023-07-01T23:17:50.000Z","size":87,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-22T12:38:02.467Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/re-js.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-07-01T14:41:58.000Z","updated_at":"2025-01-06T16:55:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"56f0969c-0f56-423e-9fe2-372c6c81093c","html_url":"https://github.com/re-js/mobx-signals","commit_stats":null,"previous_names":["betula/mobx-signals","re-js/mobx-signals"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/re-js%2Fmobx-signals","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/re-js%2Fmobx-signals/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/re-js%2Fmobx-signals/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/re-js%2Fmobx-signals/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/re-js","download_url":"https://codeload.github.com/re-js/mobx-signals/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241801194,"owners_count":20022383,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T16:31:00.173Z","updated_at":"2025-03-04T06:42:01.522Z","avatar_url":"https://github.com/re-js.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MobX Signals Implementation\n\n```bash\nnpm i mobx-signals\n```\n\nThis directory contains the code for MobX's reactive primitive, an implementation of the \"signal\" concept. A signal is a value which is \"reactive\", meaning it can notify interested consumers when it changes. There are many different implementations of this concept, with different designs:\n\n- [Angular Signals](https://github.com/angular/angular/tree/main/packages/core/src/signals)\n- [Preact Signals](https://github.com/preactjs/signals)\n- etc.\n\nThis document describes the MobX-based implementation of the signal pattern. **Basically, it totally equals Angular implementation, with an additional few MobX methods**.\n\n## Conceptual surface\n\nSignals are zero-argument functions (`() =\u003e T`). When executed, they return the current value of the signal. Executing signals does not trigger side effects, though it may lazily recompute intermediate values (lazy memoization).\n\nParticular contexts (such as template expressions) can be _reactive_. In such contexts, executing a signal will return the value, but also register the signal as a dependency of the context in question. The context's owner will then be notified if any of its signal dependencies produces a new value (usually, this results in the re-execution of those expressions to consume the new values).\n\nThis context and getter function mechanism allows for signal dependencies of a context to be tracked _automatically_ and _implicitly_. Users do not need to declare arrays of dependencies, nor does the set of dependencies of a particular context need to remain static across executions.\n\n### Writable signals: `signal()`\n\nThe `signal()` function produces a specific type of signal known as a `WritableSignal`. In addition to being a getter function, `WritableSignal`s have an additional API for changing the value of the signal (along with notifying any dependents of the change). These include the `.set` operation for replacing the signal value, `.update` for deriving a new value, and `.mutate` for performing internal mutation of the current value. These are exposed as functions on the signal getter itself.\n\n```typescript\nconst counter = signal(0);\n\ncounter.set(2);\ncounter.update(count =\u003e count + 1);\n```\n\nThe signal value can be also updated in-place, using the dedicated `.mutate` method:\n\n```typescript\nconst todoList = signal\u003cTodo[]\u003e([]);\n\ntodoList.mutate(list =\u003e {\n  list.push({title: 'One more task', completed: false});\n});\n```\n\n#### Equality\n\nThe signal creation function one can, optionally, specify an equality comparator function. The comparator is used to decide whether the new supplied value is the same, or different, as compared to the current signal’s value.\n\nIf the equality function determines that 2 values are equal it will:\n* block update of signal’s value;\n* skip change propagation.\n\n### Declarative derived values: `computed()`\n\n`computed()` creates a memoizing signal, which calculates its value from the values of some number of input signals.\n\n```typescript\nconst counter = signal(0);\n\n// Automatically updates when `counter` changes:\nconst isEven = computed(() =\u003e counter() % 2 === 0);\n```\n\nBecause the calculation function used to create the `computed` is executed in a reactive context, any signals read by that calculation will be tracked as dependencies, and the value of the computed signal recalculated whenever any of those dependencies changes.\n\nSimilarly to signals, the `computed` can (optionally) specify an equality comparator function. \n\n### Side effects: `effect()`\n\n`effect()` schedules and runs a side-effectful function inside a reactive context. Signal dependencies of this function are captured, and the side effect is re-executed whenever any of its dependencies produces a new value.\n\n```typescript\nconst counter = signal(0);\neffect(() =\u003e console.log('The counter is:', counter()));\n// The counter is: 0\n\ncounter.set(1);\n// The counter is: 1\n```\n\n## Decorators 🚀\n\nMany existing codebases use decorators, and a lot of the documentation and tutorial material online uses them as well.\n\n```typescript\nimport { signal, computed } from \"mobx-signals\";\n\nclass Todo {\n    @signal title = \"\";\n    @signal finished = false;\n\n    toggle() {\n        this.finished = !this.finished;\n    }\n}\n\nclass TodoList {\n    @signal todos = [];\n\n    @computed\n    get unfinishedTodo() {\n        return this.todos.filter(todo =\u003e !todo.finished);\n    }\n}\n```\n\nThese decorators do not need \"makeObservable\" and work in a different way, more similar to previous versions of MobX.\n\n## Additional APIs\n\nThe MobX-based library provides an excellent opportunity to export some more handy methods.\n\n### Reactions: `reaction()`\n\n```typescript\nreaction(() =\u003e value, (value, previousValue) =\u003e void, options?)\n```\n\n`reaction` is like `effect`, but gives more fine grained control on which signals will be tracked. It takes two functions: the first, data function, is tracked and returns the data that is used as input for the second, effect function. It is important to note that the side effect only reacts to data that was accessed in the data function, which might be less than the data that is actually used in the effect function.\n\nThe typical pattern is that you produce the things you need in your side effect in the data function, and in that way control more precisely when the effect triggers. By default, the result of the data function has to change in order for the effect function to be triggered. Unlike `effect`, the side effect won't run once when initialized, but only after the data expression returns a new value for the first time.\n\n### Wait for the condition once: `when()`\n\n```typescript\nwhen(predicate: () =\u003e boolean, effect?: () =\u003e void, options?)\nwhen(predicate: () =\u003e boolean, options?): Promise\n```\n\n`when` observes and runs the given predicate function until it returns true. Once that happens, the given effect function is executed and the autorunner is disposed.\n\nThe `when` function returns a disposer, allowing you to cancel it manually, unless you don't pass in a second effect function, in which case it returns a `Promise`.\n\n#### Promise of `await when(...)`\n\nIf no effect function is provided, when returns a `Promise`. This combines nicely with `async` / `await` to let you wait for changes in observable state.\n\n```typescript\nasync function() {\n    await when(() =\u003e that.isVisible)\n}\n```\n\nTo cancel `when` prematurely, it is possible to call `.cancel()` on the promise returned by itself.\n\n### `untracked()`\n\n```typescript\nuntracked(body: () =\u003e T): T\n```\n\nRuns a piece of code without establishing observers.\n\n### `transaction()`\n\n```typescript\ntransaction(body: () =\u003e T): T\n```\n\nUsed to batch a bunch of updates without running any reactions until the end of the transaction.\n\nLike `untracked` method, It takes a single, parameterless function as an argument, and returns any value that was returned by it. Note that It runs completely synchronously and can be nested. Only after completing the outermost transaction, the pending reactions will be run.\n\n\n\u003c!--\nEffects do not execute synchronously with the set (see the section on glitch-free execution below), but are scheduled and resolved by the framework. The exact timing of effects is unspecified.\n\n## Producer and Consumer\n\nInternally, the signals implementation is defined in terms of two abstractions, producers and consumers.Producers represents values which can deliver change notifications, such as the various flavors of `Signal`s. Consumers represents a reactive context which may depend on some number of producers. In other words, producers produce reactivity, and consumers consume it.\n\nImplementers of either abstraction derive from the `ReactiveNode` base class, which models participation in the reactive graph. Any `ReactiveNode` can act in the role of a producer, a consumer, or both, by interacting with the appropriate subset of APIs on `ReactiveNode`. For example, `WritableSignal`s extend `ReactiveNode` but only operate against the producer APIs, since `WritableSignal`s don't consume other signal values.\n\nSome concepts are both producers _and_ consumers. For example, derived `computed` expressions consume other signals to produce new reactive values.\n\nThroughout the rest of this document, \"producer\" and \"consumer\" are used to describe `ReactiveNode`s acting in that capacity.\n\n### The Dependency Graph\n\n`ReactiveNode`s keep track of dependency `ReactiveEdge`s to each other. Producers are aware of which consumers depend on their value, while consumers are aware of all of the producers on which they depend. These references are always bidirectional.\n\nA major design feature of Angular Signals is that dependency edges (`ReactiveEdge`s) are tracked using weak references (`WeakRef`). At any point, it's possible that a consumer node may go out of scope and be garbage collected, even if it is still referenced by a producer node (or vice versa). This removes the need for explicit cleanup operations that would remove these dependency edges for signals going \"out of scope\". Lifecycle management of signals is greatly simplified as a result, and there is no chance of memory leaks due to the dependency tracking.\n\nTo simplify tracking `ReactiveEdge`s via `WeakRef`s, `ReactiveNode`s have numeric IDs generated when they're created. These IDs are used as `Map` keys instead of the tracked node objects, which are instead stored in the `ReactiveEdge` as `WeakRef`s.\n\nAt various points during the read or write of signal values, these `WeakRef`s are dereferenced. If a reference turns out to be `undefined` (that is, the other side of the dependency edge was reclaimed by garbage collection), then the dependency `ReactiveEdge` can be cleaned up.\n--\u003e\n\n## \"Glitch Free\" property\n\nConsider the following setup:\n\n```typescript\nconst counter = signal(0);\nconst evenOrOdd = computed(() =\u003e counter() % 2 === 0 ? 'even' : 'odd');\neffect(() =\u003e console.log(counter() + ' is ' + evenOrOdd()));\n\ncounter.set(1);\n```\n\nWhen the effect is first created, it will print \"0 is even\", as expected, and record that both `counter` and `evenOrOdd` are dependencies of the logging effect.\n\nWhen `counter` is set to `1`, this invalidates both `evenOrOdd` and the logging effect. If `counter.set()` iterated through the dependencies of `counter` and triggered the logging effect first, before notifying `evenOrOdd` of the change, however, we might observe the inconsistent logging statement \"1 is even\". Eventually `evenOrOdd` would be notified, which would trigger the logging effect again, logging the correct statement \"1 is odd\".\n\nIn this situation, the logging effect's observation of the inconsistent state \"1 is even\" is known as a _glitch_. A major goal of reactive system design is to prevent such intermediate states from ever being observed, and ensure _glitch-free execution_.\n\n\u003c!--\n### Push/Pull Algorithm\n\nAngular Signals guarantees glitch-free execution by separating updates to the `ReactiveNode` graph into two phases. The first phase is performed eagerly when a producer value is changed. This change notification is propagated through the graph, notifying consumers which depend on the producer of the potential update. Some of these consumers may be derived values and thus also producers, which invalidate their cached values and then continue the propagation of the change notification to their own consumers, and so on. Other consumers may be effects, which schedule themselves for re-execution.\n\nCrucially, during this first phase, no side effects are run, and no recomputation of intermediate or derived values is performed, only invalidation of cached values. This allows the change notification to reach all affected nodes in the graph without the possibility of observing intermediate or glitchy states.\n\nOnce this change propagation has completed (synchronously), the second phase can begin. In this second phase, signal values may be read by the application or framework, triggering recomputation of any needed derived values which were previously invalidated.\n\nWe refer to this as the \"push/pull\" algorithm: \"dirtiness\" is eagerly _pushed_ through the graph when a source signal is changed, but recalculation is performed lazily, only when values are _pulled_ by reading their signals.\n--\u003e\n\n## Dynamic Dependency Tracking\n\nWhen a reactive context operation (for example, an `effect`'s side effect function) is executed, the signals that it reads are tracked as dependencies. However, this may not be the same set of signals from one execution to the next. For example, this computed signal:\n\n```typescript\nconst dynamic = computed(() =\u003e useA() ? dataA() : dataB());\n```\n\nreads either `dataA` or `dataB` depending on the value of the `useA` signal. At any given point, it will have a dependency set of either `[useA, dataA]` or `[useA, dataB]`, and it can never depend on `dataA` and `dataB` at the same time.\n\nThe potential dependencies of a reactive context are unbounded. Signals may be stored in variables or other data structures and swapped out with other signals from time to time. Thus, the signals implementation must deal with potential changes in the set of dependencies of a consumer on each execution.\n\nA naive approach would be to simply remove all old dependency edges before re-executing the reactive operation, or to mark them all as stale beforehand and remove the ones that don't get read. This is conceptually simple, but computationally heavy, especially for reactive contexts that have a largely unchanging set of dependencies.\n\n\u003c!--\n### Dependency Edge Versioning\n\nInstead, our implementation uses a lighter weight approach to dependency invalidation which relies on a monotonic version counter maintained by the consumer, called the `trackingVersion`. Before the consumer's reactive operation is executed, its `trackingVersion` is incremented. When a signal is read, the `trackingVersion` of the consumer is stored in the dependency `ReactiveEdge`, where it is available to the producer.\n\nWhen a producer has an updated value, it iterates through its outgoing edges to any interested consumers to notify them of the change. At this point, the producer can check whether the dependency is current or stale by comparing the consumer's current `trackingVersion` to the one stored on the dependency `ReactiveEdge`. A mismatch means that the consumer's dependencies have changed and no longer include that producer, so that consumer is not notified and the stale edge is instead removed.\n--\u003e\n\n## Equality Semantics\n\nProducers may lazily produce their value (such as a `computed` which only recalculates its value when pulled). However, a producer may also choose to apply an equality check to the values that it produces, and determine that the newly computed value is \"equal\" semantically to the previous. In this case, consumers which depend on that value should not be re-executed. For example, the following effect:\n\n```typescript\nconst counter = signal(0);\nconst isEven = computed(() =\u003e counter() % 2 === 0);\neffect(() =\u003e console.log(isEven() ? 'even!' : 'odd!'));\n```\n\nshould run if `counter` is updated to `1` as the value of `isEven` switches from `true` to `false`. But if `counter` is then set to `3`, `isEven` will recompute the same value: `false`. Therefore the logging effect should not run.\n\nThis is a tricky property to guarantee in our implementation because values are not recomputed during the push phase of change propagation. `isEven` is invalidated when `counter` is changed, which causes the logging `effect` to also be invalidated and scheduled. Naively, `isEven` wouldn't be recomputed until the logging effect actually runs and attempts to read its value, which is too late to notice that it didn't need to run at all.\n\n\u003c!--\n### Value Versioning\n\nTo solve this problem, our implementation uses a similar technique to tracking dependency staleness. Producers track a monotonically increasing `valueVersion`, representing the semantic identity of their value. `valueVersion` is incremented when the producer produces a semantically new value. The current `valueVersion` is saved into the dependency `ReactiveEdge` structure when a consumer reads from the producer.\n\nBefore consumers trigger their reactive operations (e.g. the side effect function for `effect`s, or the recomputation for `computed`s), they poll their dependencies and ask for `valueVersion` to be refreshed if needed. For a `computed`, this will trigger recomputation of the value and the subsequent equality check, if the value is stale (which makes this polling a recursive process as the `computed` is also a consumer which will poll its own producers). If this recomputation produces a semantically changed value, `valueVersion` is incremented.\n\nThe consumer can then compare the `valueVersion` of the new value with the one cached in its dependency `ReactiveEdge`, to determine if that particular dependency really did change. By doing this for all producers, the consumer can determine that, if all `valueVersion`s match, that no _actual_ change to any dependency has occurred, and it can skip reacting to that change (e.g. skip running the side effect function).\n\n## `Watch` primitive\n\n`Watch` is a primitive used to build different types of effects. `Watch`es are consumers that run side-effectful functions in their reactive context, but where the scheduling of the side effect is delegated to the implementor. The `Watch` will call this scheduling operation when it receives a notification that it's stale.\n\n--\u003e\n\n### Install\n\n```bash\nnpm i mobx-signals\n```\n\nEnjoy your code!","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fre-js%2Fmobx-signals","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fre-js%2Fmobx-signals","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fre-js%2Fmobx-signals/lists"}