{"id":15046960,"url":"https://github.com/lxsmnsyc/compostate","last_synced_at":"2026-03-07T05:05:53.694Z","repository":{"id":57204952,"uuid":"359490681","full_name":"lxsmnsyc/compostate","owner":"lxsmnsyc","description":"Composable and reactive state management library","archived":false,"fork":false,"pushed_at":"2025-05-09T16:36:11.000Z","size":2447,"stargazers_count":35,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-08-20T12:35:51.359Z","etag":null,"topics":["composition","preact","react","reactivity","state-management"],"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/lxsmnsyc.png","metadata":{"files":{"readme":"README.md","changelog":null,"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,"zenodo":null},"funding":{"github":"lxsmnsyc"}},"created_at":"2021-04-19T14:33:54.000Z","updated_at":"2025-06-02T12:32:59.000Z","dependencies_parsed_at":"2024-06-21T07:11:32.922Z","dependency_job_id":"4dfc5d7a-35c6-4e84-8fcf-423e35a073a3","html_url":"https://github.com/lxsmnsyc/compostate","commit_stats":{"total_commits":582,"total_committers":1,"mean_commits":582.0,"dds":0.0,"last_synced_commit":"64c9ded120b6d9d95e1618a1fa937cab4d775298"},"previous_names":[],"tags_count":102,"template":false,"template_full_name":null,"purl":"pkg:github/lxsmnsyc/compostate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fcompostate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fcompostate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fcompostate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fcompostate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lxsmnsyc","download_url":"https://codeload.github.com/lxsmnsyc/compostate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fcompostate/sbom","scorecard":{"id":81811,"data":{"date":"2022-08-15","repo":{"name":"github.com/lxsmnsyc/compostate","commit":"11f3b4cb009d9e1bff2a5c9f421d7163705d58a4"},"scorecard":{"version":"v4.5.0-26-g10b6052","commit":"10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93"},"score":5.4,"checks":[{"name":"Code-Review","score":0,"reason":"no reviews found","details":["Warn: no reviews found for commit: 11f3b4cb009d9e1bff2a5c9f421d7163705d58a4","Warn: no reviews found for commit: 4f7837796b558c45fbdf86bb3ee5222353cc44e9","Warn: no reviews found for commit: 3116c357fdc99b88410e978df5273650af54b515","Warn: no reviews found for commit: fa901d5c0ac90cfa12eb35ba845591ff51af766d","Warn: no reviews found for commit: e085d79ffbd69aa25c143eed2e447a0de675b5ad","Warn: no reviews found for commit: 1cfa332dcdcddda6bb4638b6993a030d98d461d3","Warn: no reviews found for commit: 348078f24e63326eb68be0a59afe43da100b78d6","Warn: no reviews found for commit: 26d44e1c03990e7fdd77b651ce173218c55bdaca","Warn: no reviews found for commit: 35a3ad4db030ff05f32737fdf0ccc1d3bc1c6433","Warn: no reviews found for commit: 1af93f6b0c794698dc9f1cc30543145cfaa3c3cd","Warn: no reviews found for commit: a985ff337f41d6f9d3cb3bc6369bea95773ff633","Warn: no reviews found for commit: 62775d0cc4b9a1ecba44574633d2df38b54985a7","Warn: no reviews found for commit: efb74e1a8b2b29bd63d89038ef177ecf2718c640","Warn: no reviews found for commit: 7dc28a94ee850dcaba29ec3d7b1bf21467e79449","Warn: no reviews found for commit: 312e2069513bff98fa07116ddd571c8ecb0cb985","Warn: no reviews found for commit: 1e91cc010c3d63db0694c84601d3b047ee181151","Warn: no reviews found for commit: efb7890a3d9f52de149c30b68d7a5de11996b7b7","Warn: no reviews found for commit: 99b4a0c91db2121716a9dfcf407851bcf1bae3fb","Warn: no reviews found for commit: e4eb873da1938bcf0242382d79d40ba09f68809e","Warn: no reviews found for commit: ae67cc67387fcc7cf0bdf3ffcc0af5c4bf374837","Warn: no reviews found for commit: 14466b3b9c6e6e427a05489e7c1236c82286a150","Warn: no reviews found for commit: 54afb52c2fdba8db028b7dfd0875d0e301ff3cb0","Warn: no reviews found for commit: c53fd571741bcfb631af65ada8de245f835adbd4","Warn: no reviews found for commit: 25dbe54c51722bae2c1822dd11adc4d19a1b4c1c","Warn: no reviews found for commit: 2d295fd9273dc83868e6599d60acfc03fc289dd9","Warn: no reviews found for commit: 5e3fe791672b5624fa047e7ffcf8a9201340fe08","Warn: no reviews found for commit: be9314e639545e98c774cf5500ce14024125929a","Warn: no reviews found for commit: e4f0a36a0b21cba7e079cc11a5839e7c1073817b","Warn: no reviews found for commit: 9df8f7dfdcc83f949a5b02f40a12e8e59582f2f3","Warn: no reviews found for commit: b4ff752a577d7b04b32c3af6d928465e0c79adb9"],"documentation":{"short":"Determines if the project requires code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#code-review"}},{"name":"Maintained","score":6,"reason":"8 commit(s) out of 30 and 0 issue activity out of 0 found in the last 90 days -- score normalized to 6","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no badge detected","details":null,"documentation":{"short":"Determines if the project has a CII Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"no vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#vulnerabilities"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":["Warn: no GitHub releases found"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#branch-protection"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":10,"reason":"tokens are read-only in GitHub workflows","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"no published package detected","details":["Warn: no GitHub publishing workflow detected"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#packaging"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: : LICENSE:1"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#license"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info: GitHub-owned GitHubActions are pinned","Info: Third-party GitHubActions are pinned","Info: Dockerfile dependencies are pinned","Info: no insecure (not pinned by hash) dependency downloads found in Dockerfiles","Info: no insecure (not pinned by hash) dependency downloads found in shell scripts"],"documentation":{"short":"Determines if the project has declared and pinned its dependencies.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#binary-artifacts"}},{"name":"Dependency-Update-Tool","score":0,"reason":"no update tool detected","details":["Warn: dependabot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others.","Warn: renovatebot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others."],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#dependency-update-tool"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":null,"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":null,"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/10b6052acfb4f0b8136bc4876cb6f5b6f26bfe93/docs/checks.md#fuzzing"}}]},"last_synced_at":"2025-08-15T06:03:43.399Z","repository_id":57204952,"created_at":"2025-08-15T06:03:43.399Z","updated_at":"2025-08-15T06:03:43.399Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30208730,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T03:24:23.086Z","status":"ssl_error","status_checked_at":"2026-03-07T03:23:11.444Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["composition","preact","react","reactivity","state-management"],"created_at":"2024-09-24T20:53:48.494Z","updated_at":"2026-03-07T05:05:53.662Z","avatar_url":"https://github.com/lxsmnsyc.png","language":"TypeScript","funding_links":["https://github.com/sponsors/lxsmnsyc"],"categories":[],"sub_categories":[],"readme":"# compostate\n\n\u003e Fine-grained reactivity library\n\n[![NPM](https://img.shields.io/npm/v/compostate.svg)](https://www.npmjs.com/package/compostate) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript)\n\n## Install\n\n```bash\nnpm install --save compostate\n```\n\n```bash\nyarn add compostate\n```\n\n```bash\npnpm add compostate\n```\n\n## Concepts\n\n### Signals and Atoms\n\nSignals and atoms are the main source of reactivity in `compostate`. They are akin to \"subjects\" or \"observables\" in the [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern). Signals and atoms holds values that can either be read or written.\n\n```js\nimport { signal, atom } from 'compostate';\n\n// with signals\nconst [count, setCount] = signal(0);\n// reading a signal\nconsole.log('Count', count());\n// writing to a signal\nsetCount(count() + 100);\n\n// with atoms\nconst count = atom(0);\n// reading an atom\nconsole.log('Count', count());\n// writing to an atom\ncount(count() + 100);\n```\n\n### Effects\n\nEffects are the \"observers\" of `compostate`. When reading a signal or an atom inside effects, effects will automatically mark those as \"dependencies\", in which when these dependencies update values, effects will automatically re-evaluate.\n\n```js\nimport { signal, syncEffect } from 'compostate';\n\n// Create a signal\nconst [count, setCount] = signal(0);\n\n// Observe the signal\nsyncEffect(() =\u003e {\n  console.log('Count:', count()); // Logs 'Count: 0'\n});\n\n// Update the count\nsetCount(100); // Logs 'Count: 0' due to the effect\n```\n\nWhen effects re-evaluate, it reconstructs the tracked dependencies from scratch, and so conditional dependency can also be done.\n\n```js\nsyncEffect(() =\u003e {\n  if (someCond()) {\n    // Subscribe to signalA: this effect will only evaluate\n    // if signalA changes\n    doSomething(signalA());\n  } else {\n    // Subscribe to signalB: this effect will only evaluate\n    // if signalB changes\n    doOthers(signalB());\n  }\n});\n```\n\nOne can also use `untrack` to prevent an effect from marking a signal as a dependency\n\n```js\nimport { untrack } from 'compostate';\n\nsyncEffect(() =\u003e {\n  // This effect will access `someSignal` w/o subscribing\n  const somePassiveSignal = untrack(() =\u003e someSignal());\n});\n```\n\n`syncEffect` runs synchronously with signal updates, but this might be undesirable in some cases. An alternative is `effect` which has its evaluation deferred through time-slicing.\n\n```js\nimport { effect, atom } from 'compostate';\n\nconst greeting = atom('Hello');\nconst receiver = atom('Alexis');\n\neffect(() =\u003e {\n  // Since the evaluation is deferred, this effect will only \n  // log after the synchronous code ends.\n  console.log(`${greeting()}, ${receiver()}!`);\n});\n\ngreeting('Bonjour');\nreceiver('Compostate');\n\n// At the end of this code, this logs 'Bonjour, Compostate!'\nsetTimeout(() =\u003e {\n  // The effect is now tracking greeting and receiver\n  // however like the code above, changes to the atoms\n  // would not synchronously re-evaluate the effect.\n  greeting('Hello');\n  receiver('Alexis');\n  // At the end of this callback, it logs 'Hello, Alexis!'\n}, 1000);\n```\n\n### Deriving signals\n\nSignals and atoms can be composed into derived signals. The basic form of a derived signal uses nothing but a simple function.\n\n```js\nconst count = atom(0);\nconst squared = () =\u003e count() ** 2;\n\nsyncEffect(() =\u003e {\n  console.log(squared()); // 0\n});\n\ncount(4); // 16\n```\n\nNormally this is useful but there arises a problem: a derived signal may return the same value but would still trigger a re-evaluation.\n\n```js\nconst message = atom('Hello');\nconst length = () =\u003e message().length;\n\n\nsyncEffect(() =\u003e {\n  console.log('Length:', length()); // Length: 5\n});\n\nmessage('Aloha') // Logs again with Length: 5\n```\n\nTo fix this problem, `computed` can be used in place of the derived signal.\n\n```js\nimport { computed } from 'compostate';\n\nconst message = atom('Hello');\nconst length = computed(() =\u003e message().length);\n\n\nsyncEffect(() =\u003e {\n  console.log('Length:', length()); // Length: 5\n});\n\nmessage('Aloha') // Logs nothing\nmessage('Bonjour') // Length: 7\n```\n\n`computed` keeps track of the previously returned value and compares it with the new one, deciding if it should re-evaluate its dependents.\n\n### Batching updates\n\nSignals are cheap to write with, but synchoronous updates can be expensive. For example, if an effect subscribes to multiple signals, whose values are also updated synchronously, the effect may re-evaluate multiple times which is undesirable. The desired result should be for the effect to wait for all the signals to update, and then re-evaluate so that it only has to do it a single time.\n\n`compostate` provides `batch` to group updates into a single flush.\n\n```js\nimport { syncEffect, atom, batch } from 'compostate';\n\nconst greeting = atom('Hello');\nconst receiver = atom('Alexis');\n\nsyncEffect(() =\u003e {\n  console.log(`${greeting()}, ${receiver()}!`); // 'Hello, Alexis!'\n});\n\n// Without batching\ngreeting('Bonjour'); // 'Bonjour, Alexis!'\nreceiver('Compostate'); // 'Bonjour, Compostate!'\n\n// With batching\nbatch(() =\u003e {\n  greeting('Bonjour'); // Update deferred\n  receiver('Compostate'); // Update deferred\n}); // 'Bonjour, Compostate!'\n```\n\nDo take note that in batching, writes are already applied, only the re-evaluation is deferred.\n\n`compostate` also provides `unbatch` in case flushing updates synchronously is desirable.\n\n### Cleanups\n\n`compostate` provides `onCleanup` which can be called inside tracking calls such as `syncEffect`, `computed`, etc.. Registered cleanup callbacks are evaluated before tracking call are re-evaluated. This is useful when performing side-effects like subscribing to event listeners or making requests.\n\n```js\nimport { onCleanup } from 'compostate';\n\nsyncEffect(() =\u003e {\n  const request = makeRequest(someSignal());\n\n  onCleanup(() =\u003e {\n    // When someSignal changes, make sure to cancel\n    // the current request.\n    request.cancel();\n  });\n});\n```\n\n`onCleanup` will also run if `syncEffect` or `effect` are stopped.\n\n```js\nconst stop = syncEffect(() =\u003e {\n  onCleanup(() =\u003e {\n    console.log('Stopped!');\n  });\n});\n\n// ...\nstop();\n```\n\nTracking calls are cleanup boundaries, and tracking calls are also cleaned up by their parent cleanup boundaries, so if, for example, an effect is declared inside another effect, the nested effect is stopped when the parent effect is also stopped.\n\n```js\nconst stop = syncEffect(() =\u003e {\n  syncEffect(() =\u003e {\n    onCleanup(() =\u003e {\n      console.log('Stopped!');\n    });\n  });\n});\n\n// ...\nstop();\n```\n\n`compostate` also provides `batchCleanup` which is what all tracking calls uses under the hood. `compostate` also provides `unbatchCleanup` if automatic cleanup is undesired.\n\n### Error Boundaries\n\nLike any other code, user code in effects and computations may throw an error. Normal `try`-`catch` won't work in `compostate` since by the time a re-evaluation happen, the try block may have already been escaped.\n\nTo solve this problem, `compostate` provides `errorBoundary` and `onError`.\n\n```js\nimport { errorBoundary, onError } from 'compostate';\n\nerrorBoundary(() =\u003e {\n  onError((error) =\u003e {\n    console.error(error);\n  });\n\n  // Whenever the effect re-evaluation throws\n  // the error boundary will be able to receive it.\n  effect(() =\u003e doSomeUnsafeWork());\n});\n```\n\nIf a given `onError` throws an error on itself, the thrown error and the received error is forwarded to a parent `errorBoundary`.\n\nIf there's a callback that runs outside or uncaptured by `errorBoundary` (e.g. `setTimeout`) and you want the `errorBoundary` to capture it, you can use `captureError`:\n\n```js\nimport { captureError } from 'compostate';\n\nerrorBoundary(() =\u003e {\n  onError((error) =\u003e {\n    console.error(error);\n  });\n\n  const capture = captureError();\n\n  // Whenever the effect re-evaluation throws\n  // the error boundary will be able to receive it.\n  setTimeout(() =\u003e {\n    try {\n      doSomething();\n    } catch (error) {\n      capture(error);\n    }\n  })\n});\n```\n\n### Context API\n\n`compostate` provides a way to inject values through function calls, effects and computations\n\n```js\nimport { contextual, createContext, writeContext, readContext } from 'compostate';\n\n// Create a context instance with a default value\nconst message = createContext('Hello World');\n\nfunction log() {\n  // Read the context value\n  console.log(readContext(message));\n}\n\n// Create a context boundary\ncontextual(() =\u003e {\n  // Write a context value\n  writeContext(message, 'Ohayo Sekai');\n\n  log(); // 'Ohayo Sekai'\n});\n```\n\n## Bindings\n\n- [Web Components](https://github.com/lxsmnsyc/compostate/tree/main/packages/compostate-element)\n- [React](https://github.com/lxsmnsyc/compostate/tree/main/packages/react-compostate)\n- [Preact](https://github.com/lxsmnsyc/compostate/tree/main/packages/preact-compostate)\n\n### Coming Soon\n\n- SolidJS\n- Svelte\n- Vue\n\n## License\n\nMIT © [lxsmnsyc](https://github.com/lxsmnsyc)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flxsmnsyc%2Fcompostate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flxsmnsyc%2Fcompostate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flxsmnsyc%2Fcompostate/lists"}