{"id":13657469,"url":"https://github.com/maverick-js/signals","last_synced_at":"2025-05-14T11:11:54.282Z","repository":{"id":39415322,"uuid":"506937648","full_name":"maverick-js/signals","owner":"maverick-js","description":"A tiny (~1kB minzipped) and extremely fast library for creating reactive observables via functions.","archived":false,"fork":false,"pushed_at":"2025-02-26T06:11:34.000Z","size":8743,"stargazers_count":842,"open_issues_count":9,"forks_count":23,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-03T02:06:33.581Z","etag":null,"topics":["fast","functional","isomorphic","lightweight","observables","reactive","reactivity","tiny"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maverick-js.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2022-06-24T08:37:24.000Z","updated_at":"2025-03-31T04:02:10.000Z","dependencies_parsed_at":"2024-06-18T18:14:34.974Z","dependency_job_id":"dc770db7-00ec-4927-b946-df046d0a352e","html_url":"https://github.com/maverick-js/signals","commit_stats":null,"previous_names":["mihar-22/fn-obs","maverick-js/observables"],"tags_count":101,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maverick-js%2Fsignals","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maverick-js%2Fsignals/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maverick-js%2Fsignals/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maverick-js%2Fsignals/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maverick-js","download_url":"https://codeload.github.com/maverick-js/signals/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248154986,"owners_count":21056541,"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":["fast","functional","isomorphic","lightweight","observables","reactive","reactivity","tiny"],"created_at":"2024-08-02T05:00:43.450Z","updated_at":"2025-04-10T03:44:56.042Z","avatar_url":"https://github.com/maverick-js.png","language":"TypeScript","funding_links":[],"categories":["Uncategorized","others","TypeScript"],"sub_categories":["Uncategorized"],"readme":"# Signals\n\n[![package-badge]][package]\n[![license-badge]][license]\n\n\u003e 🏆 The goal of this library is to provide a lightweight reactivity API for other UI libraries to\n\u003e be built on top of. It follows the \"lazy principle\" that Svelte adheres to - don't\n\u003e do any unnecessary work and don't place the burden of figuring it out on the developer.\n\nThis is a tiny (~1kB minzipped) library for creating reactive observables via functions called\nsignals. You can use signals to store state, create computed properties (`y = mx + b`), and subscribe\nto updates as its value changes.\n\n- 🪶 Light (~1kB minzipped)\n- 💽 Works in both browsers and Node.js\n- 🌎 All types are observable (i.e., string, array, object, etc.)\n- 🕵️‍♀️ Only updates when value has changed\n- ⏱️ Batched updates via microtask scheduler\n- 😴 Lazy by default - efficiently re-computes only what's needed\n- 🔬 Computations via `computed`\n- 📞 Effect subscriptions via `effect`\n- 🐛 Debugging identifiers\n- 💪 Strongly typed - built with TypeScript\n\n⏭️ **[Skip to API](#api)**\n\n⏭️ **[Skip to TypeScript](#typescript)**\n\n⏭️ **[Skip to Benchmarks](#benchmarks)**\n\nHere's a simple demo to see how it works:\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)][stackblitz-demo]\n\n```js\nimport { root, signal, computed, effect, tick } from '@maverick-js/signals';\n\nroot((dispose) =\u003e {\n  // Create - all types supported (string, array, object, etc.)\n  const $m = signal(1);\n  const $x = signal(1);\n  const $b = signal(0);\n\n  // Compute - only re-computed when `$m`, `$x`, or `$b` changes.\n  const $y = computed(() =\u003e $m() * $x() + $b());\n\n  // Effect - this will run whenever `$y` is updated.\n  const stop = effect(() =\u003e {\n    console.log($y());\n\n    // Called each time `effect` ends and when finally disposed.\n    return () =\u003e {};\n  });\n\n  $m.set(10); // logs `10` inside effect\n\n  // Flush queue synchronously so effect is run.\n  // Otherwise, effects will be batched and run on the microtask queue.\n  tick();\n\n  $b.set((prev) =\u003e prev + 5); // logs `15` inside effect\n\n  tick();\n\n  // Nothing has changed - no re-compute.\n  $y();\n\n  // Stop running effect.\n  stop();\n\n  // ...\n\n  // Dispose of all signals inside `root`.\n  dispose();\n});\n```\n\n## Installation\n\n```bash\n$: npm i @maverick-js/signals\n\n$: pnpm i @maverick-js/signals\n\n$: yarn add @maverick-js/signals\n```\n\n## API\n\n- [`root`](#root)\n- [`signal`](#signal)\n- [`computed`](#computed)\n- [`effect`](#effect)\n- [`peek`](#peek)\n- [`untrack`](#untrack)\n- [`readonly`](#readonly)\n- [`tick`](#tick)\n- [`computedMap`](#computedmap)\n- [`computedKeyedMap`](#computedkeyedmap)\n- [`onError`](#onerror)\n- [`onDispose`](#ondispose)\n- [`isReadSignal`](#isreadsignal)\n- [`isWriteSignal`](#iswritesignal)\n- [`getScope`](#getscope)\n- [`scoped`](#scoped)\n- [`getContext`](#getcontext)\n- [`setContext`](#setcontext)\n\n### `root`\n\nComputations are generally child computations. When their respective parent scope is destroyed so\nare they. You _can_ create orphan computations (i.e., no parent). Orphans will live in memory until\ntheir internal object references are garbage collected (GC) (i.e., dropped from memory):\n\n```js\nimport { computed } from '@maverick-js/signals';\n\nconst obj = {};\n\n// This is an orphan - GC'd when `obj` is.\nconst $b = computed(() =\u003e obj);\n```\n\nOrphans can make it hard to determine when a computation is disposed so you'll generally want to\nensure you only create child computations. The `root` function stores all inner computations as\na child and provides a function to easily dispose of them all:\n\n```js\nimport { root, signal, computed, effect } from '@maverick-js/signals';\n\nroot((dispose) =\u003e {\n  const $a = signal(10);\n  const $b = computed(() =\u003e $a());\n\n  effect(() =\u003e console.log($b()));\n\n  // Disposes of `$a`, $b`, and `effect`.\n  dispose();\n});\n```\n\n```js\n// `root` returns the result of the given function.\nconst result = root(() =\u003e 10);\n\nconsole.log(result); // logs `10`\n```\n\n### `signal`\n\nWraps the given value into a signal. The signal will return the current value when invoked `fn()`,\nand provide a simple write API via `set()`. The value can now be observed when used\ninside other computations created with [`computed`](#computed) and [`effect`](#effect).\n\n```js\nimport { signal } from '@maverick-js/signals';\n\nconst $a = signal(10);\n\n$a(); // read\n$a.set(20); // write (1)\n$a.set((prev) =\u003e prev + 10); // write (2)\n```\n\n\u003e **Warning**\n\u003e Read the [`tick`](#tick) section below to understand batched updates.\n\n### `computed`\n\nCreates a new signal whose value is computed and returned by the given function. The given\ncompute function is _only_ re-run when one of its dependencies are updated. Dependencies are\nare all signals that are read during execution.\n\n```js\nimport { signal, computed, tick } from '@maverick-js/signals';\n\nconst $a = signal(10);\nconst $b = signal(10);\nconst $c = computed(() =\u003e $a() + $b());\n\nconsole.log($c()); // logs 20\n\n$a.set(20);\ntick();\nconsole.log($c()); // logs 30\n\n$b.set(20);\ntick();\nconsole.log($c()); // logs 40\n\n// Nothing changed - no re-compute.\nconsole.log($c()); // logs 40\n```\n\n```js\nimport { signal, computed } from '@maverick-js/signals';\n\nconst $a = signal(10);\nconst $b = signal(10);\nconst $c = computed(() =\u003e $a() + $b());\n\n// Computed signals can be deeply nested.\nconst $d = computed(() =\u003e $a() + $b() + $c());\nconst $e = computed(() =\u003e $d());\n```\n\n### `effect`\n\nInvokes the given function each time any of the signals that are read inside are updated\n(i.e., their value changes). The effect is immediately invoked on initialization.\n\n```js\nimport { signal, computed, effect } from '@maverick-js/signals';\n\nconst $a = signal(10);\nconst $b = signal(20);\nconst $c = computed(() =\u003e $a() + $b());\n\n// This effect will run each time `$a` or `$b` is updated.\nconst stop = effect(() =\u003e console.log($c()));\n\n// Stop observing.\nstop();\n```\n\nYou can optionally return a function from inside the `effect` that will be run each time the\neffect re-runs and when it's finally stopped/disposed of:\n\n```js\neffect(() =\u003e {\n  return () =\u003e {\n    // Called each time effect re-runs and when disposed of.\n  };\n});\n```\n\n### `peek`\n\nReturns the current value stored inside the given compute function whilst disabling observer tracking, i.e.\nwithout triggering any dependencies. Use [`untrack`](#untrack) if you want to also disable scope tracking.\n\n```js\nimport { signal, computed, peek } from '@maverick-js/signals';\n\nconst $a = signal(10);\n\nconst $b = computed(() =\u003e {\n  // `$a` will not trigger updates on `$b`.\n  const value = peek($a);\n});\n```\n\n### `untrack`\n\nReturns the current value inside a signal whilst disabling both scope _and_ observer\ntracking. Use [`peek`](#peek) if only observer tracking should be disabled.\n\n```js\nimport { signal, effect, untrack } from '@maverick-js/signals';\n\neffect(() =\u003e {\n  untrack(() =\u003e {\n    // `$a` is now an orphan and also not tracked by the outer effect.\n    const $a = signal(10);\n  });\n});\n```\n\n### `readonly`\n\nTakes in the given signal and makes it read only by removing access to write operations (i.e.,\n`set()`).\n\n```js\nimport { signal, readonly } from '@maverick-js/signals';\n\nconst $a = signal(10);\nconst $b = readonly($a);\n\nconsole.log($b()); // logs 10\n\n// We can still update value through `$a`.\n$a.set(20);\n\nconsole.log($b()); // logs 20\n```\n\n### `tick`\n\nBy default, signal updates are batched on the microtask queue which is an async process. You can\nflush the queue synchronously to get the latest updates by calling `tick()`.\n\n\u003e **Note**\n\u003e You can read more about microtasks on [MDN][mdn-microtasks].\n\n```js\nimport { signal } from '@maverick-js/signals';\n\nconst $a = signal(10);\n\n$a.set(10);\n$a.set(20);\n$a.set(30); // only this write is applied\n```\n\n```js\nimport { signal, tick } from '@maverick-js/signals';\n\nconst $a = signal(10);\n\n// All writes are applied.\n$a.set(10);\ntick();\n$a.set(20);\ntick();\n$a.set(30);\n```\n\n### `computedMap`\n\n\u003e **Note**\n\u003e Same implementation as [`indexArray`](https://www.solidjs.com/docs/latest/api#indexarray) in Solid JS.\n\u003e Prefer [`computedKeyedMap`](#computedkeyedmap) when referential checks are required.\n\nReactive map helper that caches each item by index to reduce unnecessary mapping on updates.\nIt only runs the mapping function once per item and adds/removes as needed. In a non-keyed map like\nthis the index is fixed but value can change (opposite of a keyed map).\n\n```js\nimport { signal, tick } from '@maverick-js/signals';\nimport { computedMap } from '@maverick-js/signals/map';\n\nconst source = signal([1, 2, 3]);\n\nconst map = computedMap(source, (value, index) =\u003e {\n  return {\n    i: index,\n    get id() {\n      return value() * 2;\n    },\n  };\n});\n\nconsole.log(map()); // logs `[{ i: 0, id: $2 }, { i: 1, id: $4 }, { i: 2, id: $6 }]`\n\nsource.set([3, 2, 1]);\ntick();\n\n// Notice the index `i` remains fixed but `id` has updated.\nconsole.log(map()); // logs `[{ i: 0, id: $6 }, { i: 1, id: $4 }, { i: 2, id: $2 }]`\n```\n\n### `computedKeyedMap`\n\n\u003e **Note**\n\u003e Same implementation as [`mapArray`](https://www.solidjs.com/docs/latest/api#maparray) in Solid JS.\n\u003e Prefer [`computedMap`](#computedmap) when working with primitives to avoid unnecessary re-renders.\n\nReactive map helper that caches each list item by reference to reduce unnecessary mapping on\nupdates. It only runs the mapping function once per item and then moves or removes it as needed. In\na keyed map like this the value is fixed but the index changes (opposite of non-keyed map).\n\n```js\nimport { signal, tick } from '@maverick-js/signals';\nimport { computedKeyedMap } from '@maverick-js/signals/map';\n\nconst source = signal([{ id: 0 }, { id: 1 }, { id: 2 }]);\n\nconst nodes = computedKeyedMap(source, (value, index) =\u003e {\n  const div = document.createElement('div');\n\n  div.setAttribute('id', String(value.id));\n  Object.defineProperty(div, 'i', {\n    get() {\n      return index();\n    },\n  });\n\n  return div;\n});\n\nconsole.log(nodes()); // [{ id: 0, i: $0 }, { id: 1, i: $1 }, { id: 2, i: $2 }];\n\nsource.set((prev) =\u003e {\n  // Swap index 0 and 1\n  const tmp = prev[1];\n  prev[1] = prev[0];\n  prev[0] = tmp;\n  return [...prev]; // new array\n});\n\ntick();\n\n// No nodes were created/destroyed, simply nodes at index 0 and 1 switched.\nconsole.log(nodes()); // [{ id: 1, i: $0 }, { id: 0, i: $1 }, { id: 2, i: $2 }];\n```\n\n### `onError`\n\nRuns the given function when an error is thrown in a child scope. If the error is thrown again\ninside the error handler, it will trigger the next available parent scope handler.\n\n```js\nimport { effect, onError } from '@maverick-js/signals';\n\neffect(() =\u003e {\n  onError((error) =\u003e {\n    // ...\n  });\n});\n```\n\n### `onDispose`\n\nRuns the given function when the parent scope computation is being disposed of.\n\n```js\nimport { effect, onDispose } from '@maverick-js/signals';\n\nconst listen = (type, callback) =\u003e {\n  window.addEventListener(type, callback);\n  // Called when the effect is re-run or finally disposed.\n  onDispose(() =\u003e window.removeEventListener(type, callback));\n};\n\nconst stop = effect(\n  listen('click', () =\u003e {\n    // ...\n  }),\n);\n\nstop(); // `onDispose` is called\n```\n\nThe `onDispose` callback will return a function to clear the disposal early if it's no longer\nrequired:\n\n```js\neffect(() =\u003e {\n  const dispose = onDispose(() =\u003e {});\n  // ...\n  // Call early if it's no longer required.\n  dispose();\n});\n```\n\n### `isReadSignal`\n\nWhether the given value is a readonly signal.\n\n```js\n// True\nisReadSignal(10);\nisReadSignal(() =\u003e {});\nisReadSignal(signal(10));\nisReadSignal(computed(() =\u003e 10));\nisReadSignal(readonly(signal(10)));\n\n// False\nisReadSignal(false);\nisReadSignal(null);\nisReadSignal(undefined);\n```\n\n### `isWriteSignal`\n\nWhether the given value is a write signal (i.e., can produce new values via write API).\n\n```js\n// True\nisWriteSignal(signal(10));\n\n// False\nisWriteSignal(false);\nisWriteSignal(null);\nisWriteSignal(undefined);\nisWriteSignal(() =\u003e {});\nisWriteSignal(computed(() =\u003e 10));\nisWriteSignal(readonly(signal(10)));\n```\n\n### `getScope`\n\nReturns the currently executing parent scope.\n\n```js\nroot(() =\u003e {\n  const scope = getScope(); // returns `root` scope.\n\n  effect(() =\u003e {\n    const $a = signal(0);\n    getScope(); // returns `effect` scope.\n  });\n});\n```\n\n### `scoped`\n\nRuns the given function in the given scope so context and error handling continue to work.\n\n```js\nimport { root, getScope, scoped } from '@maverick-js/signals';\n\nroot(() =\u003e {\n  const scope = getScope();\n\n  // Timeout will lose tracking of the current scope.\n  setTimeout(() =\u003e {\n    scoped(() =\u003e {\n      // Code here will run with root scope.\n    }, scope);\n  }, 0);\n});\n```\n\n### `getContext`\n\nAttempts to get a context value for the given key. It will start from the parent scope and\nwalk up the computation tree trying to find a context record and matching key. If no value can be\nfound `undefined` will be returned. This is intentionally low-level so you can design a context API\nin your library as desired.\n\nIn your implementation make sure to check if a parent scope exists via `getScope()`. If one does\nnot exist log a warning that this function should not be called outside a computation or render\nfunction.\n\n\u003e **Note**\n\u003e See the `setContext` code example below for a demo of this function.\n\n### `setContext`\n\nAttempts to set a context value on the parent scope with the given key. This will be a no-op if\nno parent scope is defined. This is intentionally low-level so you can design a context API in your\nlibrary as desired.\n\nIn your implementation make sure to check if a parent scope exists via `getScope()`. If one does\nnot exist log a warning that this function should not be called outside a computation or render\nfunction.\n\n```js\nimport { root, getContext, setContext } from '@maverick-js/signals';\n\nconst key = Symbol();\n\nroot(() =\u003e {\n  setContext(key, 100);\n  // ...\n  root(() =\u003e {\n    const value = getContext(key); // 100\n  });\n});\n```\n\n## Debugging\n\nThe `signal`, `computed`, and `effect` functions accept a debugging ID (string) as part\nof their options.\n\n```js\nimport { signal, computed } from '@maverick-js/signals';\n\nconst $foo = signal(10, { id: 'foo' });\n```\n\n\u003e **Note**\n\u003e This feature is only available in a development or testing Node environment (i.e., `NODE_ENV`).\n\n## TypeScript\n\n```ts\nimport {\n  isReadSignal,\n  isWriteSignal,\n  type Effect,\n  type ReadSignal,\n  type WriteSignal,\n  type MaybeSignal,\n} from '@maverick-js/signals';\n\n// Types\nconst signal: ReadSignal\u003cnumber\u003e;\nconst computed: ReadSignal\u003cstring\u003e;\nconst effect: Effect;\n\n// Provide generic if TS fails to infer correct type.\nconst $a = computed\u003cstring\u003e(() =\u003e /* ... */);\n\nconst $b: MaybeSignal\u003cnumber\u003e;\n\nif (isReadSignal($b)) {\n  $b(); // ReadSignal\u003cnumber\u003e\n}\n\nif (isWriteSignal($b)) {\n  $b.set(10); // WriteSignal\u003cnumber\u003e\n}\n```\n\n## Benchmarks\n\n### Layers\n\nThis benchmark was taken from [`cellx`](https://github.com/Riim/cellx#benchmark). It\ntests how long it takes for an `n` deeply layered computation to update. The benchmark can be\nfound [here](./bench/layers.js).\n\nEach column represents how deep computations were layered. The average time taken to update the\ncomputation out of a 100 runs is used for each library.\n\n\u003cimg src=\"./bench/layers.png\" alt=\"Layers benchmark table\" width=\"350px\" /\u003e\n\n#### Notes\n\n- Nearly all computations in a real world app are going to be less than 10 layers deep, so only the\n  first column really matters.\n- This benchmark favours eagerly scheduling computations and aggresive caching in a single long\n  computation subtree. This is not a great benchmark for signals libraries as it doesn't measure\n  what really matters such as dynamic graph updates, source/observer changes, and scope disposals.\n\n### Reactively\n\nThis benchmark was taken from [`reactively`](https://github.com/modderme123/reactively). It sets\nup various computation graphs with a set number of sources (e.g., `1000x5` is 1000 computations with\na tree depth of 5). The benchmark measures how long it takes for changes to be applied after static\nor dynamic updates are made to the graph (i.e., pick a node and update its value).\n\n\u003cimg src=\"./bench/reactively.png\" alt=\"Reactively benchmark charts\" /\u003e\n\n#### Notes\n\n- This assumes Solid JS is in batch-only mode which is not realistic as a real world app won't\n  have batch applied everywhere.\n\n## Inspiration\n\n`@maverick-js/signals` was made possible based on code and learnings from:\n\n- [Reactively][reactively]\n- [Solid JS][solidjs]\n- [Sinuous][sinuous]\n- [Hyperactiv][hyperactiv]\n- [Svelte Scheduler][svelte-scheduler]\n\nSpecial thanks to Modderme, Wesley, Julien, and Solid/Svelte contributors for all their work 🎉\n\n[package]: https://www.npmjs.com/package/@maverick-js/signals\n[package-badge]: https://img.shields.io/npm/v/@maverick-js/signals/latest\n[license]: https://github.com/maverick-js/signals/blob/main/LICENSE\n[license-badge]: https://img.shields.io/github/license/maverick-js/signals\n[size-badge]: https://img.shields.io/bundlephobia/minzip/@maverick-js/signals@^5.0.0\n[reactively]: https://github.com/modderme123/reactively\n[solidjs]: https://github.com/solidjs/solid\n[sinuous]: https://github.com/luwes/sinuous\n[hyperactiv]: https://github.com/elbywan/hyperactiv\n[svelte-scheduler]: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/scheduler.ts\n[mdn-microtasks]: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide\n[stackblitz-demo]: https://stackblitz.com/edit/maverick-signals?embed=1\u0026file=index.ts\u0026hideExplorer=1\u0026hideNavigation=1\u0026view=editor\n[bundlephobia]: https://bundlephobia.com/package/@maverick-js/signals@^5.0.0\n[maverick-scheduler]: https://github.com/maverick-js/scheduler\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaverick-js%2Fsignals","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaverick-js%2Fsignals","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaverick-js%2Fsignals/lists"}