ngssm-store
v21.1.1
Published
NgSsm - Simple state management implementation.
Readme
ngssm-store
A lightweight, production-ready state management library for Angular applications based on the Redux pattern. Provides centralized state management with full support for reducers, effects, and modern Angular signals.
Overview
ngssm-store is a simple yet powerful custom implementation of the Redux pattern designed specifically for Angular. It leverages Angular's dependency injection, RxJS for reactive updates, and modern Angular signals for optimal reactivity.
Key Features
- Centralized State Management: Single source of truth for application state
- Redux Pattern: Actions → Reducers → State → Effects → Actions
- Dual Reactivity: Both RxJS observables and Angular Signals support
- Immutable State: Uses
immutability-helperto ensure state immutability - Effect System: Side effects, async operations, and action chaining
- Feature States: Modular state management with feature-based organization
- Action Queue: Sequential action processing for predictable state updates
- Logging & Debugging: Built-in logging system for monitoring state changes
- TypeScript Support: Fully typed for better developer experience
- Dependency Injection: Leverages Angular's DI system
Architecture
Redux Flow
Component/Effect
↓ (dispatch)
Action → Store → Action Queue
↓
Process Next Action
↓
Apply to Reducers
↓
Update State Immutably
↓
Publish New State
↓
Process Effects
↓
(Can dispatch actions)Installation
npm install ngssm-storePeer Dependencies
@angular/core>= 20.0.0@angular/common>= 20.0.0immutability-helper>= 3.1.1
Setup
Global Provider
Initialize the store in your application bootstrapping:
import { provideNgssmStore } from 'ngssm-store';
bootstrapApplication(AppComponent, {
providers: [
provideNgssmStore(),
]
});Core Concepts
Actions
Actions are plain objects that describe what happened. They must have a type property:
export interface Action {
type: string;
}
// Example action class
export class IncrementCounterAction implements Action {
readonly type = 'INCREMENT_COUNTER';
constructor(public readonly amount: number = 1) {}
}
export class LoadUsersAction implements Action {
readonly type = 'LOAD_USERS';
}Reducers
Reducers are pure functions that take the current state and an action, then return a new state. They must be immutable:
import { Reducer, State, Action } from 'ngssm-store';
import update from 'immutability-helper';
@Injectable()
export class CounterReducer implements Reducer {
processedActions = ['INCREMENT_COUNTER', 'DECREMENT_COUNTER'];
updateState(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT_COUNTER':
return update(state, {
counter: {
value: { $apply: (v: number) => v + (action as IncrementCounterAction).amount }
}
});
case 'DECREMENT_COUNTER':
return update(state, {
counter: { value: { $set: state.counter.value - 1 } }
});
default:
return state;
}
}
}Effects
Effects handle side effects like API calls, logging, and dispatching new actions. They don't modify state:
import { Effect, ActionDispatcher, State, Action } from 'ngssm-store';
@Injectable()
export class UserEffect implements Effect {
processedActions = ['LOAD_USERS'];
private readonly userService = inject(UserService);
constructor(private injector: EnvironmentInjector) {}
processAction(dispatcher: ActionDispatcher, state: State, action: Action): void {
if (action.type === 'LOAD_USERS') {
this.userService.getUsers().subscribe((users) => {
dispatcher.dispatchAction(new SetUsersAction(users));
});
}
}
}State
The state is a plain object containing all application data. Structure it hierarchically:
export interface State {
counter?: {
value: number;
};
users?: {
list: User[];
loading: boolean;
error?: Error;
};
settings?: {
theme: string;
language: string;
};
}Usage
Dispatching Actions
import { Store } from 'ngssm-store';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
`
})
export class CounterComponent {
private store = inject(Store);
count = createSignal((state) => state.counter?.value ?? 0);
increment() {
this.store.dispatchAction(new IncrementCounterAction(1));
}
decrement() {
this.store.dispatchAction(new DecrementCounterAction());
}
}Accessing State with Signals
Use the signal-based API for reactive components:
import { createSignal } from 'ngssm-store';
@Component({
selector: 'app-dashboard',
template: `
<div>
<p>Total Users: {{ userCount() }}</p>
<div *ngIf="loading()">Loading...</div>
<ul>
<li *ngFor="let user of users()">{{ user.name }}</li>
</ul>
</div>`
})
export class DashboardComponent {
private store = inject(Store);
users = createSignal((state) => state.users?.list ?? []);
loading = createSignal((state) => state.users?.loading ?? false);
userCount = createSignal((state) => (state.users?.list ?? []).length);
}Accessing State with RxJS
For components that need RxJS integration:
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users$ | async">{{ user.name }}</li>
</ul>`
})
export class UserListComponent {
private store = inject(Store);
users$ = this.store.state$.pipe(
map((state) => state.users?.list ?? [])
);
}Direct State Access
Access the current state directly:
@Component(...)
export class MyComponent {
private store = inject(Store);
getCurrentState() {
const currentState = this.store.state(); // Signal access
// or
const viaObservable = this.store.state$; // Observable access
}
}Tracking Processed Actions
Monitor which action was last processed:
@Component(...)
export class MyComponent {
private store = inject(Store);
lastAction = createSignal((state) => this.store.processedAction().type);
// Or with RxJS
lastAction$ = this.store.processedAction$.pipe(map(a => a.type));
}Registering Reducers and Effects
Single Reducer/Effect
import { provideReducer, provideEffect } from 'ngssm-store';
bootstrapApplication(AppComponent, {
providers: [
provideNgssmStore(),
provideReducer(CounterReducer),
provideEffect(UserEffect)
]
});Multiple Reducers/Effects
import { provideReducers, provideEffects } from 'ngssm-store';
bootstrapApplication(AppComponent, {
providers: [
provideNgssmStore(),
provideReducers(
CounterReducer,
UserReducer,
SettingsReducer
),
provideEffects(
UserEffect,
NotificationEffect
)
]
});Effect Functions
Effect functions are the modern replacement for the Effect interface. They are executed in an injection context, allowing you to use Angular's inject function for dependency injection:
import { provideEffectFunc } from 'ngssm-store';
import { inject } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
provideNgssmStore(),
provideEffectFunc('LOAD_USERS', (state, action) => {
const userService = inject(UserService);
const dispatcher = inject(ACTION_DISPATCHER);
userService.getUsers().subscribe((users) => {
dispatcher.dispatchAction(new SetUsersAction(users));
});
})
]
});Effect functions should be preferred over the legacy Effect interface implementation as they are more concise and leverage Angular's dependency injection system.
Feature States
Organize state by feature for better modularity:
import { NgSsmFeatureState } from 'ngssm-store';
@NgSsmFeatureState({
featureStateKey: 'products',
initialState: {
list: [],
loading: false,
selectedId: null
}
})
export class ProductFeatureState {}
// Access in components
products = createSignal((state) => state.products?.list ?? []);State Initializers
Initialize state with data from external sources:
import { StateInitializer } from 'ngssm-store';
@Injectable()
export class AppInitializer implements StateInitializer {
private configService = inject(ConfigService);
initializeState(state: State): State {
const config = this.configService.getConfig();
return update(state, {
settings: { $set: config }
});
}
}
// Provide it
bootstrapApplication(AppComponent, {
providers: [
provideNgssmStore(),
{ provide: NGSSM_STATE_INITIALIZER, useClass: AppInitializer }
]
});Action Processing
Sequential Processing
Actions are processed sequentially using an action queue:
- Action dispatched
- Added to queue
- Store schedules processing (via setTimeout by default)
- Reducers update state
- State published to all subscribers
- Effects process action (can dispatch new actions)
- Next action processed
Macro Tasks vs Micro Tasks
By default, the store uses setTimeout (macro-tasks) for action processing. You can switch to micro-tasks (Promises) if needed:
@Injectable()
export class Store {
// Set to false for micro-tasks (Promise.resolve())
public useMacroTasks = true;
}Best Practices
- Keep Actions Simple: Actions should be serializable plain objects
- Pure Reducers: Never mutate state directly; always use
immutability-helper - Avoid Side Effects in Reducers: Use Effects for side effects
- Type Your State: Define clear State interfaces
- Use Signals for Performance: Prefer
createSignalover RxJS when possible in new code - Single Responsibility: One reducer per feature/domain
- Logging: Use the Logger service for debugging
- Action Names: Use clear, descriptive action type names (FEATURE_ACTION_NAME pattern)
- Immutability: Never modify state objects in reducers
- Error Handling: Handle errors in effects and dispatch error actions
API Reference
Store Class
state(): Signal<State>- Get current state as a Signalstate$: Observable<State>- Get state as an ObservableprocessedAction(): Signal<Action>- Get last processed action as a SignalprocessedAction$: Observable<Action>- Get last processed action as ObservabledispatchAction(action: Action): void- Dispatch an actiondispatchActionType(actionType: string): void- Dispatch by action type string
Helper Functions
createSignal<T>(selector: (state: State) => T): Signal<T>- Create a derived signal from stateprovideNgssmStore()- Initialize the storeprovideReducer(reducer)- Register a single reducerprovideReducers(...reducers)- Register multiple reducersprovideEffect(effect)- Register a single effectprovideEffects(...effects)- Register multiple effectsprovideEffectFunc(actionType, func)- Register an effect function
Interfaces
Action- Action interface withtypepropertyReducer- Reducer interface withprocessedActionsandupdateState()Effect- Effect interface withprocessedActionsandprocessAction()State- Base state type (empty object by default)StateInitializer- Interface for initializing stateActionDispatcher- Interface for dispatching actions
Debugging
Logging
Enable logging to monitor state changes and action processing:
import { Logger } from 'ngssm-store';
constructor(private logger: Logger) {
this.logger.information('Component initialized');
}DevTools Integration
The store can be integrated with Redux DevTools for advanced debugging (requires additional setup).
Performance Considerations
- Signals: Prefer signals over observables for better performance in modern Angular
- Selectors: Use
createSignalwith specific selectors to minimize re-renders - Memoization: Consider memoizing expensive selector functions
- Action Batching: Dispatch related actions together to reduce re-renders
Dependencies
- Angular Core: Dependency injection, signals, lifecycle management
- RxJS: Reactive state and action streams
- immutability-helper: Safe state immutability patterns
- TypeScript: Full type safety
License
MIT
