{"id":46109569,"url":"https://github.com/irvrodflo/ngx-state-crafter","last_synced_at":"2026-03-13T17:00:47.362Z","repository":{"id":339881065,"uuid":"1163703862","full_name":"irvrodflo/ngx-state-crafter","owner":"irvrodflo","description":"Simple and predictable state management for Angular 17+. No actions, no reducers — just signals.","archived":false,"fork":false,"pushed_at":"2026-02-22T19:22:48.000Z","size":80,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-23T00:07:33.487Z","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":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/irvrodflo.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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-22T02:28:10.000Z","updated_at":"2026-02-22T19:22:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/irvrodflo/ngx-state-crafter","commit_stats":null,"previous_names":["irvrodflo/ng-state-crafter","irvrodflo/ngx-state-crafter"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/irvrodflo/ngx-state-crafter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-state-crafter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-state-crafter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-state-crafter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-state-crafter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/irvrodflo","download_url":"https://codeload.github.com/irvrodflo/ngx-state-crafter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-state-crafter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30471114,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T11:00:43.441Z","status":"ssl_error","status_checked_at":"2026-03-13T11:00:23.173Z","response_time":60,"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":[],"created_at":"2026-03-01T22:00:55.620Z","updated_at":"2026-03-13T17:00:47.347Z","avatar_url":"https://github.com/irvrodflo.png","language":"TypeScript","funding_links":[],"categories":["State Management"],"sub_categories":["Other State Libraries"],"readme":"# ngx-state-crafter\n\nA lightweight, reactive state management library for Angular, built on top of Angular Signals.\n\n**ngx-state-crafter** gives you structure without boilerplate. No actions, no reducers, no decorators — just a simple API that scales from a single component to complex feature states.\n\n---\n\n## Requirements\n\n- Angular 17+\n\n---\n\n## Installation\n\n```bash\nnpm install @irv-labs/ngx-state-crafter\n```\n\n---\n\n## Quick Start\n\n```typescript\nimport { craftState } from '@irv-labs/ngx-state-crafter';\n\ninterface CounterState {\n  count: number;\n  label: string;\n}\n\nconst state = craftState\u003cCounterState\u003e({\n  count: 0,\n  label: 'My Counter',\n});\n\n// Read values as signals\nconsole.log(state.count()); // 0\n\n// Update state\nstate.update({ count: 1 });\n\n// Update with a function\nstate.update({ count: (prev) =\u003e prev + 1 });\n```\n\n---\n\n## API\n\n### `craftState\u003cT\u003e(initial: T): StateInstance\u003cT\u003e`\n\nCreates a new reactive state instance from an initial object.\n\n```typescript\nconst state = craftState({ count: 0, name: 'Guest' });\n```\n\n---\n\n### `.update(updater)`\n\nUpdates one or more properties. Accepts a partial object or a function that receives the current state.\n\n```typescript\n// Direct value\nstate.update({ count: 10 });\n\n// Function updater\nstate.update({ count: (prev) =\u003e prev + 1 });\n\n// Multiple properties at once\nstate.update({ count: 0, name: 'Alice' });\n\n// Function that returns partial state\nstate.update((s) =\u003e ({ count: s.count + 1 }));\n```\n\n---\n\n### `.select\u003cK\u003e(key)`\n\nReturns the signal for a specific property.\n\n```typescript\nconst count = state.select('count'); // Signal\u003cnumber\u003e\ncount(); // read current value\n```\n\n---\n\n### `.computed(projector)`\n\nCreates a derived signal from the state. Automatically updates when dependencies change.\n\n```typescript\nconst doubled = state.computed((s) =\u003e s.count * 2);\nconst label = state.computed((s) =\u003e `${s.name}: ${s.count}`);\n```\n\n---\n\n### `.effect(fn)`\n\nRuns a side effect whenever the state changes. Returns an `EffectRef`.\n\n```typescript\nconst ref = state.effect((s) =\u003e {\n  console.log('State changed, count is:', s.count);\n});\n\n// Cleanup when done\nref.destroy();\n```\n\n---\n\n### `.watch\u003cK\u003e(key, fn)`\n\nWatches a single property and runs a callback when it changes. Returns an `EffectRef`.\n\n```typescript\nconst ref = state.watch('count', (value) =\u003e {\n  console.log('Count is now:', value);\n});\n```\n\n---\n\n### `.when(predicate, fn)`\n\nRuns a callback when a condition becomes `true`. Returns an `EffectRef`.\n\n```typescript\nconst ref = state.when(\n  (s) =\u003e s.count \u003e 10,\n  () =\u003e console.log('Count exceeded 10!'),\n);\n```\n\n---\n\n### `.merge\u003cK\u003e(key, partial)`\n\nPartially updates a nested object property without replacing the entire value.\n\n```typescript\ninterface AppState {\n  user: { name: string; role: string };\n}\n\nconst state = craftState\u003cAppState\u003e({\n  user: { name: 'Guest', role: 'viewer' },\n});\n\n// Only updates role, keeps name intact\nstate.merge('user', { role: 'admin' });\n```\n\n---\n\n### `.snapshot()`\n\nReturns the current state as a plain, non-reactive object.\n\n```typescript\nconst snap = state.snapshot();\nconsole.log(snap); // { count: 5, name: 'Alice' }\n```\n\n---\n\n### `.reset()`\n\nResets the state back to its initial values.\n\n```typescript\nstate.reset();\n```\n\n---\n\n### `.debug\u003cK\u003e(key, label?)`\n\nLogs a property's value to the console whenever it changes. Meant for development only.\n\n```typescript\nstate.debug('count', 'MyComponent');\n// [State Debug: MyComponent] count -\u003e 5\n```\n\n---\n\n## Usage in a Component\n\n```typescript\nimport { Component } from '@angular/core';\nimport { craftState } from '@irv-labs/ngx-state-crafter';\n\ninterface FormState {\n  email: string;\n  submitted: boolean;\n}\n\n@Component({\n  selector: 'app-form',\n  template: `\n    \u003cinput [value]=\"email()\" (input)=\"setEmail($any($event.target).value)\" /\u003e\n    \u003cbutton [disabled]=\"!isValid()\" (click)=\"submit()\"\u003eSubmit\u003c/button\u003e\n  `,\n})\nexport class FormComponent {\n  state = craftState\u003cFormState\u003e({ email: '', submitted: false });\n\n  email = this.state.select('email');\n  isValid = this.state.computed((s) =\u003e s.email.includes('@'));\n\n  constructor() {\n    this.state.when(\n      (s) =\u003e s.submitted,\n      () =\u003e console.log('Form submitted!'),\n    );\n  }\n\n  setEmail(value: string) {\n    this.state.update({ email: value });\n  }\n\n  submit() {\n    this.state.update({ submitted: true });\n  }\n}\n```\n\n---\n\n## Usage in a Service\n\n`craftState` works great inside Angular services for shared or feature-level state.\n\n```typescript\nimport { Injectable } from '@angular/core';\nimport { craftState } from '@irv-labs/ngx-state-crafter';\n\ninterface CartState {\n  items: { id: number; name: string; price: number }[];\n  discount: number;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class CartService {\n  private state = craftState\u003cCartState\u003e({ items: [], discount: 0 });\n\n  items = this.state.select('items');\n  total = this.state.computed((s) =\u003e s.items.reduce((sum, i) =\u003e sum + i.price, 0) - s.discount);\n\n  addItem(item: { id: number; name: string; price: number }) {\n    this.state.update({ items: (prev) =\u003e [...prev, item] });\n  }\n\n  applyDiscount(amount: number) {\n    this.state.update({ discount: amount });\n  }\n\n  reset() {\n    this.state.reset();\n  }\n}\n```\n\n---\n\n## TypeScript Support\n\n`ngx-state-crafter` is fully typed. All methods infer types from your state interface automatically.\n\n```typescript\ninterface MyState {\n  count: number;\n  name: string;\n}\n\nconst state = craftState\u003cMyState\u003e({ count: 0, name: '' });\n\nstate.update({ count: 'oops' }); // Type error\nstate.select('unknown'); // Type error\nstate.merge('count', {}); // Type error — count is not an object\n```\n\n---\n\n## Safety\n\nAll callbacks passed to `watch`, `when`, and `effect` are internally wrapped with `untracked`. This means you can safely read or write other signals inside these callbacks without risking infinite reactive loops.\n\n---\n\n## Path Alias (optional)\n\nIf you prefer a shorter import, configure a path alias in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@state-crafter\": [\"./node_modules/@irv-labs/ngx-state-crafter\"]\n    }\n  }\n}\n```\n\nThen import from the alias instead:\n\n```typescript\n// Before\nimport { craftState } from '@irv-labs/ngx-state-crafter';\n\n// After\nimport { craftState } from '@state-crafter';\n```\n\n\u003e This is purely a local convenience — it does not affect the published package or your teammates unless they add the same alias to their `tsconfig.json`.\n\n---\n\n## Philosophy\n\n- **No boilerplate** — one function call to get a fully reactive state\n- **Signal-native** — built entirely on Angular Signals, no RxJS required\n- **Predictable** — safe by default, no surprise loops\n- **Minimal API** — learn it in 10 minutes, use it anywhere\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firvrodflo%2Fngx-state-crafter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Firvrodflo%2Fngx-state-crafter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firvrodflo%2Fngx-state-crafter/lists"}