npm package: @ngsig/store
Description: Angular-native signal-first state management. Actions + decorators from NgRx/NGXS, zero RxJS in components.
Author: Jose Estrella
Latest version: 0.1.0

# @ngsig/store

> **ng** + **si**gnal — Angular-native, signal-first state management.

Combines the best of NgRx and NGXS: action classes with decorators, a single state tree, and first-class Angular Signals. **Zero RxJS in your components.**

---

## Why @ngsig/store?

| Feature                       | NgRx        | NGXS | **@ngsig/store** |
| ----------------------------- | ----------- | ---- | ---------------- |
| Action classes                | Factory fns | ✅   | ✅               |
| Decorator-based handlers      | ❌          | ✅   | ✅               |
| Native `Signal<T>` output     | ❌          | ❌   | ✅               |
| No `toSignal()` bridge needed | ❌          | ❌   | ✅               |
| `computed()` selectors        | ❌          | ❌   | ✅               |
| Effects stay in RxJS          | ✅          | ✅   | ✅               |
| Redux DevTools                | ✅          | ✅   | ✅               |

---

## Installation

```bash
npm install @ngsig/store
```

**Peer dependencies:** `@angular/core >= 17`, `rxjs >= 7`

---

## Quick Start

### 1. Define actions

```typescript
export class LoginSuccess {
	static readonly type = '[Auth] Login Success';
	constructor(
		public user: CurrentUser,
		public accessToken: string
	) {}
}

export class Logout {
	static readonly type = '[Auth] Logout';
}
```

### 2. Define state

```typescript
import { SignalState, On, Effect } from '@ngsig/store';

interface AuthStateModel {
	isAuthenticated: boolean;
	currentUser: CurrentUser | null;
}

@SignalState({
	name: 'auth',
	defaults: { isAuthenticated: false, currentUser: null }
})
export class AuthState {
	@On(LoginSuccess)
	loginSuccess(state: AuthStateModel, action: LoginSuccess): Partial<AuthStateModel> {
		return { isAuthenticated: true, currentUser: action.user };
	}

	@On(Logout)
	logout(): Partial<AuthStateModel> {
		return { isAuthenticated: false, currentUser: null };
	}

	@Effect(LoginSuccess)
	onLogin(action: LoginSuccess) {
		// Return an Observable/Promise to dispatch follow-up actions
		return this.http.post('/audit/login', { user: action.user });
	}
}
```

### 3. Define selectors

```typescript
import { createSelector } from '@ngsig/store';

// Selectors are factory functions — call them in injection context
export const selectCurrentUser = createSelector(AuthState, (s) => s.currentUser);
export const selectIsAuthenticated = createSelector(AuthState, (s) => s.isAuthenticated);

// Compose with computed()
export function selectDisplayName(): Signal<string> {
	const user = selectCurrentUser();
	return computed(() => user()?.name ?? 'Guest');
}
```

### 4. Register in app.config.ts

```typescript
import { provideSignalStore, withDevtools } from '@ngsig/store';

export const appConfig: ApplicationConfig = {
	providers: [provideSignalStore([AuthState], withDevtools({ disabled: environment.production }))]
};
```

### 5. Use in components — pure signals, no RxJS

```typescript
import { selectCurrentUser, selectIsAuthenticated } from './auth.selectors';
import { injectDispatch } from '@ngsig/store';

@Component({ ... })
export class AppComponent {
  // Signals — no toSignal(), no async pipe, no subscriptions
  protected readonly currentUser     = selectCurrentUser();
  protected readonly isAuthenticated = selectIsAuthenticated();
  protected readonly displayName     = selectDisplayName();

  private readonly dispatch = injectDispatch();

  login() {
    this.dispatch(new LoginSuccess(user, token));
  }
}
```

---

## API Reference

### Decorators

| Decorator                          | Target | Description                                                     |
| ---------------------------------- | ------ | --------------------------------------------------------------- |
| `@SignalState({ name, defaults })` | Class  | Marks a class as a state slice                                  |
| `@On(...ActionClasses)`            | Method | Pure state handler — return `Partial<State>`                    |
| `@Effect(...ActionClasses)`        | Method | Side-effect handler — return `Observable`, `Promise`, or `void` |

### DI

| Function                                    | Description                                |
| ------------------------------------------- | ------------------------------------------ |
| `provideSignalStore(states[], ...features)` | Register store + states in `app.config.ts` |
| `injectState(StateClass)`                   | Inject raw `Signal<StateModel>`            |
| `injectDispatch()`                          | Inject `(action: object) => void`          |
| `createSelector(StateClass, projector)`     | Create a memoized `Signal<R>` factory      |

### Features

| Function                | Description                                  |
| ----------------------- | -------------------------------------------- |
| `withDevtools(config?)` | Redux DevTools browser extension integration |

### Testing

```typescript
import { provideSignalStoreTesting, dispatchInTest } from '@ngsig/store/testing';

await TestBed.configureTestingModule({
	imports: [MyComponent],
	providers: [provideSignalStoreTesting(AuthState)]
}).compileComponents();

const store = TestBed.inject(SignalStore);
dispatchInTest(store, new LoginSuccess(mockUser, 'token'));
```