{"id":16731757,"url":"https://github.com/spearwolf/signalize","last_synced_at":"2025-03-21T21:31:24.983Z","repository":{"id":57161909,"uuid":"407088700","full_name":"spearwolf/signalize","owner":"spearwolf","description":"A standalone library for signals and effects on the web","archived":false,"fork":false,"pushed_at":"2025-03-21T07:56:47.000Z","size":32840,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-21T08:01:35.226Z","etag":null,"topics":["effects","event-driven-programming","events","javascript","microfrontends","reactive","signals"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spearwolf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-16T08:41:10.000Z","updated_at":"2025-03-21T07:56:51.000Z","dependencies_parsed_at":"2022-08-24T10:30:30.447Z","dependency_job_id":"3e842f0e-cf00-41cb-8f8a-d585134b41f1","html_url":"https://github.com/spearwolf/signalize","commit_stats":{"total_commits":48,"total_committers":1,"mean_commits":48.0,"dds":0.0,"last_synced_commit":"a486fde71f2efec16c95d389bbaa2b88779339b0"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spearwolf%2Fsignalize","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spearwolf%2Fsignalize/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spearwolf%2Fsignalize/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spearwolf%2Fsignalize/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spearwolf","download_url":"https://codeload.github.com/spearwolf/signalize/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244874176,"owners_count":20524576,"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":["effects","event-driven-programming","events","javascript","microfrontends","reactive","signals"],"created_at":"2024-10-12T23:38:46.021Z","updated_at":"2025-03-21T21:31:19.972Z","avatar_url":"https://github.com/spearwolf.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# @spearwolf/signalize\n\nsignals and effects for all 📢\n\n![signalize hero image](hero-web.webp)\n\u003csmall\u003e\u003cem\u003eImage created in response to a request from spearwolf, using OpenAI's DALL-E, guided by ChatGPT.\u003c/em\u003e\u003c/small\u003e\n\n---\n\n![npm (scoped)](https://img.shields.io/npm/v/%40spearwolf/signalize)\n![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/spearwolf/signalize/main.yml)\n![GitHub](https://img.shields.io/github/license/spearwolf/signalize)\n\n`@spearwolf/signalize` is a javascript library for creating __signals__ and __effects__.\n\n- a standalone javascript library that is framework agnostic\n- without side-effects and targets `ES2022` based environments\n- written in typescript v5 and uses the new [tc39 decorators](https://github.com/tc39/proposal-decorators) :rocket:\n- however, it is optional and not necessary to use the decorators\n\n# ⚙️ Install\n\n```shell\nnpm install @spearwolf/signalize\n```\n\nPackaged as `ES2022` and exported as _unbundled_ ESM-only javascript modules.\nType definitions and source maps also included.\n\n\u003e [!NOTE]\n\u003e Since `v0.5` there is also a [CHANGELOG](https://github.com/spearwolf/signalize/blob/main/CHANGELOG.md) 🎉\n\n\u003e [!CAUTION]\n\u003e Since `v0.7` _commonjs_ modules are no longer exported❗\n\n# Overview 👀\n\nThe whole API of `@spearwolf/signalize` is about ..\n\n- __Signals__\n  - like state variables with hidden superpowers\n  - when the value of a signal changes, all observers are automatically informed\n- __Effects__\n  - are functions that are _automatically executed_ when one or more signals change\n  - just think of it as a next-gen and independent `useEffect()` hook (but without the limitations imposed by react :wink:)\n\nA __functional API__ is provided, as well as a __class-based API that uses decorators__.\n\n\u003e [!NOTE]\n\u003e Under the hood the event-driven micro-library [@spearwolf/eventize](https://github.com/spearwolf/eventize) is used 😍\n\n\n# 📖 Usage\n\n\u003e [!WARNING] \n\u003e The core of the library is stable and fully tested, although the API is still partially evolving, and the same goes for the documentation ... there are some features that are not documented in detail here. The adventurous developer is encouraged to explore the source code and tests directly at this point.\n\n## API Overview\n\n- **Signals**\n  - **create**\n    - `🦋 = {get: λ, set: setλ} = createSignal()`\n    - `@signal() accessor α`\n  - **read**\n    - `🦋.get()`\n    - `λ()`\n    - `🦋.onChange(callback)`\n    - `λ(callback)`\n    - `🦋.value`\n    - `value(λ)`\n    - `beQuiet(callback)`\n  - **write**\n    - `🦋.set(value)`\n    - `setλ(value)`\n    - `🦋.touch()`\n    - `touch(λ)`\n    - `batch(callback)`\n    - `🦋.muted`\n    - `muteSignal(λ)`\n    - `unmuteSignal(λ)`\n  - **destroy**\n    - `🦋.destroy()`\n    - `destroySignal(λ)`\n  - **object helpers**\n    - `findObjectSignalByName(🦋, name)`\n    - `findObjectSignalNames(🦋)`\n    - `findObjectSignals(🦋)`\n    - `destroyObjectSignals(🦋)`\n- **Effects**\n  - **create**\n    - *dynamic*\n      - `🦄 = createEffect(callback)`\n      - `🦄 = createEffect(callback, options)`\n    - *static*\n      - `🦄 = createEffect(callback, [...dependencies])`\n      - `🦄 = createEffect(callback, options)`\n      - `🦋.onChange(callback)`\n      - `λ(callback)`\n  - **api**\n    - `🦄.run()`\n    - `🦄.destroy()`\n- **Memo**\n  - `λ = createMemo(callback)`\n  - `@memo() compute() { .. }`\n- **Building Blocks**\n  - *connections between signals*\n    - `γ = link(src, trgt)`\n      - `γ.nextValue(): Promise`\n      - `γ.asyncValues(): yield*`\n      - `γ.touch()`\n      - `γ.mute()`\n      - `γ.unmute()`\n      - `γ.toggle()`\n      - `γ.isMuted`\n      - `γ.destroy()`\n      - `γ.isDestroyed`\n    - `unlink()`\n  - _signal groups_\n    - `SignalGroup.get(obj)` \u0026rarr; *group*\n    - `SignalGroup.findOrCreate(obj)` \u0026rarr; *group*\n    - `SignalGroup.destroy(obj)`\n    - `SignalGroup.clear()`\n    - `SignalGroup#attachGroup(group)`\n    - `SignalGroup#detachGroup(group)`\n    - `SignalGroup#attachSignal(🦋|λ)`\n    - `SignalGroup#detachSignal(🦋|λ)`\n    - `SignalGroup#attachSignalByName(name, 🦋|λ)`\n    - `SignalGroup#hasSignal(name)` \u0026rarr; _boolean_\n    - `SignalGroup#signal(name)` \u0026rarr; `🦋`\n    - `SignalGroup#attachEffect(🦄)`\n    - `SignalGroup#runEffects()`\n    - `SignalGroup#attachLink(link)`\n    - `SignalGroup#detachLink(link)`\n    - `SignalGroup#destroy()`\n- **utils**\n  - `isSignal(🦋|λ)`\n  - `muteSignal(🦋|λ)`\n  - `unmuteSignal(🦋|λ)`\n- **testing**\n  - `getSignalsCount()`\n  - `getEffectsCount()`\n  - `getLinksCount()`\n\n\n## 📖 Signals\n\nSignals are mutable states that can trigger effects when changed.\n\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003cth\u003eA standalone signal\u003c/th\u003e\n      \u003cth\u003eA class with a signal\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd valign=\"top\"\u003e\n        \u003cpicture\u003e\n          \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-standalone-signal--dark.png\"\u003e\n          \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-standalone-signal--light.png\"\u003e\n          \u003cimg\n            src=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-standalone-signal--light.png\"\n            alt=\"A standalone signal\"\n            style=\"max-width: 100%;\"\n          /\u003e\n        \u003c/picture\u003e\n      \u003c/td\u003e\n      \u003ctd valign=\"top\"\u003e\n        \u003cpicture\u003e\n          \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-class-with-a-signal--dark.png\"\u003e\n          \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-class-with-a-signal--light.png\"\u003e\n          \u003cimg\n            src=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-class-with-a-signal--light.png\"\n            alt=\"A class with a signal\"\n            style=\"max-width: 100%;\"\n          /\u003e\n        \u003c/picture\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n### Create a signal\n\n#### API\n\n```js\n🦋 = {get: λ, set: setλ} = createSignal()\n\n⋯ = createSignal(initialValue)\n⋯ = createSignal(initialValue, options)\n```\n\n##### Return value\n\n`createSignal()` \u0026rarr; `🦋 | {get: signalReader, set: signalWriter}` returns the _signal object_ (🦋), which contains the _signal reader_ and the _signal writer_ functions.\n\nIf the _signal reader_ is called as a function, it returns the current _signal value_ as the return value: `λ(): value`\n\nIf the _signal writer_ is called with a value, this value is set as the new _signal value_: `setλ(nextValue)`\nWhen the signal value _changes_, any _effects_ that depend on it will be executed.\n\nReading and writing is always immediate. Any effects are called synchronously. However, it is possible to change this behavior using `batch()`, `beQuiet()`, `value()` or other methods of this library. \n\nThe _signal object_ (🦋) is a wrapper around it, providing a signal API beyond read and write:\n\n| 🦋-Methods | Description |\n|------------|-------------|\n| \u003ccode\u003e.get()\u0026nbsp;\u0026rarr;\u0026nbsp;value\u003c/code\u003e | The _signal reader_ returns the value. If the method is called during a _dynamic effect_, the effect is informed of this and the next time the value changes, the effect is automatically repeated. |\n| `.set(value)` | The _signal writer_ sets the new value and informs the observers of the new value. |\n| `.value` | Just return the value. This is done without noticing any effect, as opposed to using `.get()` |\n| \u003ccode\u003e.onChange((value)\u0026nbsp;\u0026rarr;\u0026nbsp;void)\u003c/code\u003e | ... |\n| `.muted` | ... |\n| `.touch()` | ... |\n| `.destroy()` | ... |\n\n\n\u003e [!NOTE]\n\u003e You can destroy the reactivity of a signal with `🦋.destroy()` or `destroySignal(λ)`.\n\u003e **A destroyed signal will no longer trigger any effects**. But both the _signal reader_ and the _signal writer_ are still usable and will read and write the _signal value_.\n\n\n\n##### createSignal() Options\n\n| option         | type                | description |\n| -------------- | ------------------- | ----------- |\n| `compare`    | \u003ccode\u003e(a,\u0026nbsp;b)\u0026nbsp;=\u003e\u0026nbsp;boolean\u003c/code\u003e | Normally, the equality of two values is checked with the strict equality operator `===`. If you want to go a different way here, you can pass a function that does this. |\n| `lazy`         | `boolean`           | If this flag is set, it is assumed that the value is a function that _returns the current value_. This function is then executed _lazy_, i.e. only when the signal is read for the first time. At this point, however, it should be noted that the _signal value_ is initially only _lazy_. once resolved, it is no longer _lazy_. |\n| `beforeRead` | \u003ccode\u003e()\u0026nbsp;=\u003e\u0026nbsp;void\u003c/code\u003e | the name says it all: a callback that is executed before the signal value is read. not intended for everyday use, but quite useful for edge cases and testing. |\n\n\n### Create a signal using decorators\n\n```js\nimport {signal} from '@spearwolf/signalize/decorators';\nimport {findObjectSignalByName} from '@spearwolf/signalize';\n\nclass Foo {\n  @signal() accessor foo = 'bar';\n  @signal({readAsValue: true}) accessor xyz = 123;\n}\n\nconst obj = new Foo();\n\nobj.foo;             // =\u003e 'bar'\nobj.foo = 'plah';    // set value to 'plah'\n\nobj.xyz;             // =\u003e 123\nobj.xyz = 456;       // set value to 456\n\nfindObjectSignalByName.get(obj, 'xyz').value // =\u003e 456\n```\n\n#### API\n\n##### `@signal`\n\n```js\nclass {\n  \n  @signal() accessor Λ = initialValue\n\n  @signal(options) accessor Λ = initialValue\n\n}\n```\n\n| option        | type                 | description |\n| ------------- | -------------------- | ----------- |\n| `name`        | `string`\u0026nbsp;\\|\u0026nbsp;`symbol` | The name of the signal. setting a name is optional, the signal name is usually the same as the _accessor_ name. each object has an internal map of its signals, where the key is the signal name. the name is used later, for example, for `findObjectSignalByName()` or `destroySignal()` |\n| `readAsValue` | `boolean`            | If enabled, the value of the signal will be read without informing the dependencies, just like the `value(λ)` helper does. However, if the signal was defined as an object accessor using the decorator, it is not possible to access the signal object without the `findObjectSignalByName()` helper. |\n\n\n### Read signal value\n\n\n```typescript\nλ(): val\n🦋.get(): val\n```\n\nCalling the _signal reader_ without arguments returns the value of the signal. If this _is called up within a dynamic effect_, the effect remembers this signal and marks it as a dependent signal.\n\n```js\nvalue(λ|🦋): val\n🦋.value\n```\nreturns the value of the signal. in contrast to the previous variant, however, **no effect is notified here**. it really only returns the value, there are no side effects.\n\n```js\nbeQuiet(callback)\n```\nexecutes the callback immediately. if a signal is read out within the callback, this is done without notifying an active dynamic effect. it does not matter whether the signal is read out directly or with the `value()` helper.\n\n\n### Write signal value\n\n```js\nsetλ(value) \n🦋.set(val)\n```\nCalling the _signal writer_ sets a new signal value. if the value changes (this is normally simply checked using the `===` operator), all effects that have marked this signal as a dependency are executed immediately.\n\n```js\ntouch(λ|🦋)\n🦋.touch()\n```\ndoes not change the value of the signal. however, all dependent effects are still notified and executed.\n\n```js\nbatch(callback)\n```\nexecutes the callback immediately. if values are changed within the callback signal, the values are changed immediately - but any dependent effects are only executed once after the end of the callback. this prevents effects with multiple dependencies from being triggered multiple times if several signals are written.\n\nSee [The difference between the standard behavior of effects and the use of batching](./docs/AdvancedGuide.md#the-difference-between-the-standard-behavior-of-effects-and-the-use-of-batching) for more informations on this.\n\n\n### Destroy signal\n\n```js\ndestroySignal(λ|🦋)\n🦋.destroy()\n```\n\nDestroys the _reactivity_ of the signal. This signal will no longer be able to cause any effects.\nHowever, the _signal reader_ and _signal writer_ functions will continue to work as expected.\n\n\n## 📖 Effects\n\nEffects are functions that react to changes in signals and are executed automatically.\n\nWithout effects, signals are nothing more than ordinary variables.\n\nWith effects, you can easily control behavior changes in your application without having to write complex dependency or monitoring logic.\n\n### Dynamic vs. Static effects\n\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003cth\u003eA dynamic effect function\u003c/th\u003e\n      \u003cth\u003eA class with a dynamic effect\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd valign=\"top\"\u003e\n        \u003cpicture\u003e\n          \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-standalone-effect-function--dark.png\"\u003e\n          \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-standalone-effect-function--light.png\"\u003e\n          \u003cimg\n            src=\"https://github.com/spearwolf/signalize/raw/main/docs/images/gists/a-standalone-effect-function--light.png\"\n            alt=\"A standalone effect function\"\n            style=\"max-width: 100%;\"\n          /\u003e\n        \u003c/picture\u003e\n      \u003c/td\u003e\n      \u003ctd valign=\"top\"\u003e\n        \u003cpicture\u003e\n          \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-class-with-an-effect-method--dark.png\"\u003e\n          \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/spearwolf/signalize/main/docs/images/gists/a-class-with-an-effect-method--light.png\"\u003e\n          \u003cimg\n            src=\"https://github.com/spearwolf/signalize/raw/main/docs/images/gists/a-class-with-an-effect-method--light.png\"\n            alt=\"A class with an effect method\"\n            style=\"max-width: 100%;\"\n          /\u003e\n        \u003c/picture\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n**Dynamic effects** are always executed the first time. During the execution of an effect callback function, the read signals are tracked. If one of the signals is changed afterwards, the effect is (automatically) called again.\n\n\u003e [!NOTE]\n\u003e The signals used are re-recorded each time the effect runs again.\n\u003e This is why they are called _dynamic_ effects.\n\n**Static effects** do not track signals; instead, dependencies are defined in advance during effect creation:\n\n```js\ncreateEffect(() =\u003e {\n  const sum = a() + b();\n  console.log('sum of', a(), 'and', b(), 'is', sum);\n}, [a, b]);\n```\n\nIt doesn't matter which signals are used within the effect function, the effect will be re-run whenever a signal in the signal dependencies list changes.\n\n\n### API\n\n#### Static Effects\n\n```js\n🦄 = {run, destroy} = createEffect(callback, [...dependencies])\n🦄 = {run, destroy} = createEffect(callback, options)\n```\n\n| option      | type                          | description |\n| ----------- | ----------------------------- | ----------- |\n| `dependencies`      | `Array\u003c` λ \\| `string` \\| `symbol` `\u003e` | these are the signal dependencies that mark this as a _static_ effect. otherwise it is a _dynamic_ effect. the effect is only executed when the dependent signals change. in contrast to the dynamic effects, it does not matter which signals are used within the effect. |\n| `autorun`   | `boolean`                     | if _autorun_ is set to `false`, the effect callback will not be called automatically at any time! to call the effect, you must explicitly call the `run()` function. everything else behaves as expected for an effect. when `run()` is called, the effect is only executed when the signals have changed (or on the very first call). |\n\n```js\nλ(effectCallback)\n```\nalternatively, the _signal reader_ can also be called with an effect callback. this creates a _static_ effect that is called whenever the signal value changes. important here: the callback is not called automatically the first time, but only when the _signal value_ changes afterwards.\n\n\u003e [!NOTE]\n\u003e By the way, you cannot directly destroy an effect created in this way, this happens automatically when the signal is destroyed.\n\n\n#### Dynamic Effects\n\n```js\n🦄 = {run, destroy} = createEffect(callback)\n🦄 = {run, destroy} = createEffect(callback, options)\n```\n\n| option      | type                          | description |\n| ----------- | ----------------------------- | ----------- |\n| `autorun`   | `boolean`                     | if _autorun_ is set to `false`, the effect callback will not be called automatically at any time! to call the effect, you must explicitly call the `run()` function. everything else behaves as expected for an effect. when `run()` is called, the effect is only executed when the signals have changed (or on the very first call). |\n\n\n#### The return value of `createEffect()`\n\nThe call to `createEffect()` returns an effect object.\n\nHere you can find the `run()` function. When the _run_ function is called, the effect is executed, but only if the dependent signals have changed.\n\nSo this function is not really useful unless you use the `autorun: false` feature, which prevents the effect from being executed automatically.\n\nThis is where the `run()` comes in, which explicitly executes the effect: for example, do you want to execute an effect only at a certain time (e.g. within a `setInterval()` or `requestAnimationFrame()` callback)? then `run()` is the way to go!\n\nThe effect object also contains the destroy callback, which destroys the effect when called.\n\n\n### The effect can optionally return a _cleanup_ function\n\nYour _effect callback_ (which is your function that you pass to the effect as parameter) may also optionally return a _cleanup_ function.\n\nBefore calling an _effect_, a previously set _cleanup_ function is executed.\n\nThe effect cleanup function is reset each time the effect is executed. If the effect does not return a function, nothing will be called the next time the effect is called.\n\n\u003e [!NOTE]\n\u003e Does this behavior look familiar? probably because this feature was inspired by [react's useEffect hook](https://react.dev/reference/react/useEffect)\n\n#### Example: Use an effect _cleanup_ function\n\n```js\nconst {get: getSelector, set: makeInteractive} = createSignal();\n\nfunction onClick(event) {\n  console.log('click! selector=', getSelector(), 'element=', event.target);\n}\n\ncreateEffect(() =\u003e {\n  if (getSelector()) {\n    const el = document.querySelector(getSelector());\n\n    el.addEventListener('click', onClick, false);\n\n    return () =\u003e {\n      el.removeEventListener('click', onClick, false);\n    };\n  }\n})\n\nmakeInteractive('#foo');  // foo is now interactive\nmakeInteractive('.bar');  // bar is now interactive, but foo is not\n```\n\n\n---\n\n_more docs coming!!_\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspearwolf%2Fsignalize","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspearwolf%2Fsignalize","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspearwolf%2Fsignalize/lists"}