Stato est une librairie de gestion d'état Angular moderne pour remplacer NgRx complètement, avec une API plus simple, Signals-first, sans RxJS obligatoire.
| ❌ NgRx v20 (officiel, MIT) | ✅ ngStato |
|---|---|
loadStudents: rxMethod<void>( |
async loadStudents(state) { |
pipe( |
state.isLoading = true |
tap(() => patchState(store, { isLoading: true })), |
state.students = await service.getAll() |
switchMap(() => |
state.isLoading = false |
from(service.getAll()).pipe( |
} |
tapResponse({ |
|
next: (s) => patchState(store, { students: s, isLoading: false }), |
1 concept : async/await |
error: (e) => patchState(store, { error: e.message, isLoading: false }) |
5 lignes |
}) |
|
) |
|
) |
|
) |
|
) |
|
| 9 concepts RxJS/NgRx — 14 lignes |
1 concept au lieu de 9 pour écrire une action async NgRx nécessite rxMethod, pipe, tap, switchMap, from, tapResponse, patchState... ngStato nécessite uniquement async/await — natif JavaScript.
2x moins de code pour le même résultat Sur un store CRUD complet (5 actions), NgRx v20 nécessite ~90 lignes. ngStato nécessite ~45 lignes.
DevTools sans extension browser NgRx DevTools nécessite l'extension Chrome Redux DevTools. ngStato intègre ses DevTools directement dans l'app — fonctionnels sur tous les browsers et mobile.
Protection production automatique
ngStato utilise isDevMode() d'Angular — les DevTools sont physiquement impossibles à activer en prod.
38x plus léger — ~3 KB vs ~50 KB gzip
npm install @ngstato/core @ngstato/angular// app.config.ts
import { provideStato } from '@ngstato/angular'
import { isDevMode } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideStato({
http: { baseUrl: 'https://api.monapp.com', timeout: 8000 },
devtools: isDevMode()
})
]
}// user.store.ts
import { createStore, http, optimistic, retryable, connectDevTools } from '@ngstato/core'
import { StatoStore, injectStore } from '@ngstato/angular'
function createUserStore() {
const store = createStore({
// State
users: [] as User[],
isLoading: false,
error: null as string | null,
// Computed — recalculés automatiquement
computed: {
total: (state) => state.users.length,
admins: (state) => state.users.filter(u => u.role === 'admin')
},
// Actions
actions: {
// Chargement avec retry automatique
loadUsers: retryable(
async (state) => {
state.isLoading = true
state.users = await http.get('/users')
state.isLoading = false
},
{ attempts: 3, backoff: 'exponential', delay: 1000 }
),
// Suppression avec rollback automatique
deleteUser: optimistic(
(state, id: string) => {
state.users = state.users.filter(u => u.id !== id)
},
async (_, id: string) => {
await http.delete(`/users/${id}`)
}
),
// Action simple
async addUser(state, user: UserCreate) {
const created = await http.post('/users', user)
state.users = [...state.users, created]
}
},
// Hooks lifecycle
hooks: {
onError: (err, name) => console.error(`[UserStore] ${name}:`, err.message)
}
})
connectDevTools(store, 'UserStore') // ← une seule ligne
return store
}
// Injection auto + API classe sans boilerplate
export const UserStore = StatoStore(() => {
return createUserStore()
})
// Dans un composant
@Component({ ... })
export class UserListComponent {
store = injectStore(UserStore) // ou inject(UserStore)
}| Helper | Description | Équivalent NgRx |
|---|---|---|
abortable() |
Annule la requête précédente automatiquement | switchMap |
debounced() |
Debounce sans RxJS — défaut 300ms | debounceTime |
throttled() |
Throttle sans RxJS | throttleTime |
exclusive() |
Ignore les nouveaux appels pendant qu'une exécution est en cours | exhaustMap |
retryable() |
Retry avec backoff fixe ou exponentiel | retryWhen |
queued() |
Met en file et exécute les appels dans l'ordre d'arrivée | concatMap |
createEntityAdapter() |
Collections normalisées (ids/entities) + CRUD/selectors | NgRx Entity Adapter |
withEntities() |
Wrapper de config pour injecter slice entities + actions/selectors | feature + adapter entity |
pipeStream() + operators |
Streams composables sans RxJS obligatoire | pipe + operators RxJS |
fromStream() |
Écoute Observable/WebSocket/Firebase/Supabase | rxMethod + Effect |
optimistic() |
Update immédiat + rollback automatique si échec | Manuel en NgRx |
withPersist() |
Persistance state (localStorage/sessionStorage) + migration | Meta-reducers custom |
import { abortable, debounced, throttled, exclusive, retryable, queued, fromStream, optimistic } from '@ngstato/core'
actions: {
// Annulation auto — comme switchMap
search: abortable(async (state, q: string, { signal }) => {
state.results = await fetch(`/api/search?q=${q}`, { signal }).then(r => r.json())
}),
// exclusive — ignore les nouveaux appels pendant la requête en cours
searchExclusive: exclusive(async (state, q: string) => {
state.results = await fetch(`/api/search?q=${q}`).then(r => r.json())
}),
// queued — met en file et exécute dans l'ordre d'arrivée
searchQueued: queued(async (state, q: string) => {
state.results = await fetch(`/api/search?q=${q}`).then(r => r.json())
}),
// Debounce 300ms
filter: debounced((state, q: string) => { state.query = q }, 300),
// Retry x3 avec backoff exponentiel
load: retryable(async (state) => {
state.data = await http.get('/data')
}, { attempts: 3, backoff: 'exponential' }),
// Realtime WebSocket
listen: fromStream(
() => webSocket('wss://api.monapp.com/ws'),
(state, msg) => { state.messages = [...state.messages, msg] }
),
// Optimistic + rollback auto
delete: optimistic(
(state, id) => { state.items = state.items.filter(i => i.id !== id) },
async (_, id) => { await http.delete(`/items/${id}`) }
)
}selectorsmemoïzés avec recalcul cibléeffectsréactifs explicites avec cleanupwithPersist()pour hydrate/persist avec versioning
import { http } from '@ngstato/core'
// Configurer via provideStato() — une seule fois
provideStato({
http: {
baseUrl: 'https://api.monapp.com',
timeout: 8000,
headers: { 'X-App-Version': '1.0' },
auth: () => localStorage.getItem('token')
}
})
// Utiliser partout dans les actions
await http.get('/users')
await http.get('/users', { params: { page: 1, limit: 10 } })
await http.post('/users', { name: 'Alice' })
await http.put('/users/1', { name: 'Bob' })
await http.patch('/users/1', { active: false })
await http.delete('/users/1')Panel intégré dans l'app — sans extension browser, sans installation supplémentaire.
- Panel déplaçable à la souris
- Redimensionnable — coin bas-droite
- Minimisable — bouton ▼/▲
- Historique des actions avec durées et timestamps
- Diff Avant/Après pour chaque action
- Onglet State — state actuel complet
- Désactivé automatiquement en production via
isDevMode()
// app.config.ts
provideStato({ devtools: isDevMode() })
// app.component.ts
import { StatoDevToolsComponent } from '@ngstato/angular'
@Component({
imports: [RouterOutlet, StatoDevToolsComponent],
template: `<router-outlet /><stato-devtools />`
})
export class AppComponent {}
// mon-store.ts
connectDevTools(store, 'MonStore') // une seule ligne| NgRx DevTools | ngStato DevTools | |
|---|---|---|
| Installation | Extension Chrome | Zéro installation |
| Browser support | Chrome/Firefox | Tous browsers |
| Mobile | ❌ | ✅ |
| Désactivé en prod | Manuel | isDevMode() auto |
| State visible en prod | Oui si oubli | Jamais |
La migration est progressive — store par store.
// withState → state initial
// NgRx
withState({ users: [] as User[], isLoading: false })
// ngStato
users: [] as User[], isLoading: false,
// withMethods + rxMethod → actions
// NgRx
withMethods((store) => ({
load: rxMethod<void>(pipe(
tap(() => patchState(store, { isLoading: true })),
switchMap(() => from(service.get()).pipe(
tapResponse({
next: (d) => patchState(store, { data: d, isLoading: false }),
error: (e) => patchState(store, { error: e.message })
})
))
))
}))
// ngStato
actions: {
async load(state) {
state.isLoading = true
state.data = await service.get()
state.isLoading = false
}
}
// withComputed → computed
// NgRx
withComputed((store) => ({
total: computed(() => store.users().length)
}))
// ngStato
computed: {
total: (state) => state.users.length
}| Feature | NgRx SignalStore v20 | ngStato v0.3 |
|---|---|---|
| withState | ✅ | ✅ |
| withMethods / actions | ✅ rxMethod requis | ✅ async/await |
| withComputed | ✅ | ✅ |
| patchState | ✅ obligatoire | ✅ state.x = y |
| provideStore | ✅ | ✅ provideStato() |
| inject() | ✅ | ✅ injectStore() |
| onInit / onDestroy | ✅ | ✅ |
| DevTools | ✅ extension Chrome | ✅ panel intégré |
| DevTools mobile | ❌ | ✅ |
| Protection prod | ✅ isDevMode() auto | |
| RxJS requis | ✅ obligatoire | ❌ optionnel |
| Bundle size | ~50 KB gzip | ~3 KB gzip |
| withProps | ✅ | 🔜 |
| withEntities | ✅ | ✅ (createEntityAdapter + withEntities) |
| Concurrency helpers | ✅ via RxJS operators | ✅ (abortable, exclusive, queued) |
| Async composition | ✅ via RxJS (forkJoin, race) |
✅ (forkJoin, race) |
| Stream operators | ✅ RxJS complet | ✅ pipeStream + operators (RxJS optionnel) |
| signalStoreFeature() | ✅ | 🔜 v0.4 |
| Schematics CLI | ✅ | 🔜 v1.0 |
| ESLint plugin | ✅ | 🔜 v1.0 |
- Spécification détaillée:
README-SPECD.md - Site docs (GitHub Pages): becher.github.io/ngstato
createStore()— state, actions, computed, hooksStatoHttp— GET POST PUT PATCH DELETE avec auth, timeout, paramsabortable(),debounced(),throttled(),retryable(),fromStream(),optimistic()@ngstato/angular— Signals natifs,provideStato(),injectStore()- DevTools — panel déplaçable, redimensionnable, minimisable
connectDevTools()— connexion automatique store → DevTools- Protection prod automatique via
isDevMode() - 144 tests — 100% passing
selectorsmemoïzéseffectsréactifs avec cleanupwithPersist()— localStorage / sessionStorage + migration
exclusive()— = exhaustMap NgRxqueued()— = concatMap NgRxon()— réactions inter-storesforkJoin()/race()— patterns async (Promise-first)distinctUntilChanged()— éviter les exécutions inutilescombineLatest()/combineLatestStream()— state deps + streams externes (RxJS optionnel)- Unification
init()multi-framework (core/Angular/React/Vue) pipeStream()+ operators stream (map/filter/switchMap/concatMap/exhaustMap/mergeMap/distinct/debounce/throttle/catchError/retry)createEntityAdapter()+withEntities()pour apps CRUD complexes- Testing utilities (partiellement livré, à compléter)
- DevTools time-travel (à venir)
- Schematics CLI, ESLint plugin
- Documentation VitePress complète
- Benchmarks comparatifs
git clone https://github.com/becher/ngstato
cd ngstato
pnpm install # Node >= 18, pnpm >= 8
pnpm build
pnpm test # 144 testsConvention commits : feat / fix / docs / test / refactor / chore
MIT — Copyright (c) 2025 ngStato