State management for Angular — async/await instead of RxJS, ~3 KB instead of ~50 KB.
Start in 5 min · Documentation · API Reference · NgRx Migration
NgRx — 9 concepts, 14 lines:
load: rxMethod<void>(pipe(
tap(() => patchState(store, { loading: true })),
switchMap(() => from(service.getAll()).pipe(
tapResponse({
next: (users) => patchState(store, { users, loading: false }),
error: (e) => patchState(store, { error: e.message, loading: false })
})
))
))ngStato — 1 concept, 5 lines:
async load(state) {
state.loading = true
state.users = await http.get('/users')
state.loading = false
}Same behavior. Same Signals. Same Angular DI. 87% less code.
npm install @ngstato/core @ngstato/angular// app.config.ts
import { provideStato } from '@ngstato/angular'
import { isDevMode } from '@angular/core'
export const appConfig = {
providers: [
provideStato({
http: { baseUrl: 'https://api.example.com' },
devtools: isDevMode()
})
]
}// users.store.ts
import { createStore, http, connectDevTools } from '@ngstato/core'
import { StatoStore, injectStore } from '@ngstato/angular'
export const UsersStore = StatoStore(() => {
const store = createStore({
users: [] as User[],
loading: false,
error: null as string | null,
selectors: {
total: (s) => s.users.length,
activeUsers: (s) => s.users.filter(u => u.active)
},
actions: {
async loadUsers(state) {
state.loading = true
state.error = null
try {
state.users = await http.get('/users')
} catch (e) {
state.error = (e as Error).message
throw e
} finally {
state.loading = false
}
},
async createUser(state, payload: Omit<User, 'id'>) {
const user = await http.post<User>('/users', payload)
state.users = [...state.users, user]
},
async deleteUser(state, id: string) {
await http.delete(`/users/${id}`)
state.users = state.users.filter(u => u.id !== id)
}
},
hooks: {
onInit: (store) => store.loadUsers(),
onError: (err, action) => console.error(`[UsersStore] ${action}:`, err.message)
}
})
connectDevTools(store, 'UsersStore')
return store
})// users.component.ts — all state properties are Angular Signals
@Component({
template: `
@if (store.loading()) {
<div class="spinner">Loading...</div>
}
<h2>Users ({{ store.total() }})</h2>
@for (user of store.users(); track user.id) {
<div class="user-card">
<span>{{ user.name }}</span>
<button (click)="store.deleteUser(user.id)">Delete</button>
</div>
}
<button (click)="store.loadUsers()">Refresh</button>
`
})
export class UsersComponent {
store = injectStore(UsersStore)
}Done. State is Signals. Actions are functions. No boilerplate.
| NgRx v21 | ngStato | |
|---|---|---|
| Bundle | ~50 KB gzip | ~3 KB gzip |
| Concepts for async action | 9 (rxMethod, pipe, tap, switchMap…) | 1 (async/await) |
| Lines for a CRUD store | ~90 | ~45 |
| RxJS required | Yes | No |
| DevTools | Chrome extension only | Built-in panel, all browsers, mobile |
| Time-travel | ✅ via extension | ✅ built-in with fork-on-dispatch |
| Action replay | ❌ | ✅ re-execute any action |
| State export/import | Via extension | ✅ JSON file for bug reports |
| Prod safety | Manual logOnly |
Auto isDevMode() |
| Entity adapter | ✅ | ✅ createEntityAdapter + withEntities |
| Feature composition | ✅ signalStoreFeature |
✅ mergeFeatures() |
| Service injection | ✅ withProps |
✅ withProps() + closures |
| Concurrency control | Via RxJS operators | ✅ Native helpers |
| Testing | provideMockStore |
✅ createMockStore() |
| Persistence | Custom meta-reducers | ✅ withPersist() built-in |
| Schematics CLI | ✅ ng generate |
✅ ng generate @ngstato/schematics:store |
| ESLint plugin | ✅ @ngrx/eslint-plugin |
✅ @ngstato/eslint-plugin |
import { exclusive, retryable, optimistic, abortable, queued } from '@ngstato/core'
actions: {
submit: exclusive(async (s) => { ... }), // ignore while running (exhaustMap)
search: abortable(async (s, q, { signal }) => {}), // cancel previous (switchMap)
load: retryable(async (s) => { ... }, { attempts: 3 }), // auto-retry
delete: optimistic((s, id) => { ... }, async () => { ... }), // instant + rollback
send: queued(async (s, msg) => { ... }), // process in order (concatMap)
}Plus: debounced · throttled · distinctUntilChanged · forkJoin · race · combineLatest · fromStream · pipeStream + 12 stream operators · createEntityAdapter · withEntities · withPersist · mergeFeatures · withProps · on() inter-store reactions
Built-in panel. Drag, resize, minimize. No Chrome extension.
Auto-disabled in production via isDevMode().
import { connectDevTools, devTools } from '@ngstato/core'
connectDevTools(store, 'UsersStore')
// Time-travel programmatically
devTools.undo() // step backward
devTools.redo() // step forward
devTools.travelTo(logId) // jump to any action
devTools.replay(logId) // re-execute an action
devTools.resume() // resume live mode
// Export/import for bug reports
const snapshot = devTools.exportSnapshot()
devTools.importSnapshot(snapshot)<!-- app.component.html -->
<stato-devtools /># Generate a full CRUD store with tests
ng generate @ngstato/schematics:store users --crud --entity
# Generate a reusable feature
ng generate @ngstato/schematics:feature loadingExample generated store
// users.store.ts (auto-generated)
import { createStore, http, createEntityAdapter, withEntities, connectDevTools } from '@ngstato/core'
import { StatoStore } from '@ngstato/angular'
export interface User { id: string; name: string }
const adapter = createEntityAdapter<User>()
function createUserStore() {
const store = createStore({
...withEntities<User>(),
loading: false,
error: null as string | null,
selectors: { total: (s) => s.ids.length },
actions: {
async loadUsers(state) { /* ... */ },
async createUser(state, payload) { /* ... */ },
async updateUser(state, id, changes) { /* ... */ },
async deleteUser(state, id) { /* ... */ }
},
hooks: {
onInit: (store) => store.loadUsers(),
onError: (err, action) => console.error(`[UserStore] ${action}:`, err.message)
}
})
connectDevTools(store, 'UserStore')
return store
}
export const UserStore = StatoStore(() => createUserStore())npm install -D @ngstato/eslint-plugin// eslint.config.js
import ngstato from '@ngstato/eslint-plugin'
export default [ngstato.configs.recommended]| Rule | Default | Description |
|---|---|---|
ngstato/no-state-mutation-outside-action |
error |
Prevent direct state mutation |
ngstato/no-async-without-error-handling |
warn |
Require try/catch in async actions |
ngstato/require-devtools |
warn |
Suggest connectDevTools() |
| Package | Description | Size |
|---|---|---|
@ngstato/core |
Framework-agnostic store engine + helpers | ~3 KB |
@ngstato/angular |
Angular Signals + DI + DevTools | ~1 KB |
@ngstato/testing |
createMockStore() test utilities |
< 1 KB |
@ngstato/schematics |
ng generate — store & feature scaffolding |
CLI |
@ngstato/eslint-plugin |
3 ESLint rules for best practices | CLI |
| Start in 5 min | Core concepts |
| Angular guide | Architecture |
| Testing guide | NgRx migration |
| CRUD recipe | API reference |
| Entities | Benchmarks |
git clone https://github.com/becher/ngStato
cd ngStato && pnpm install && pnpm build && pnpm testMIT — Copyright © 2025-2026 ngStato