The Ultimate Plug-N-Play State Manager for NgRx
Simplify your Angular state management with zero boilerplate and maximum productivity
Quick Start • Features • Documentation • Examples • Community
Feature | Description | Status |
---|---|---|
⚡️ Plug & Play | Get started in under 2 minutes | ✅ |
🔥 Zero Boilerplate | No actions, reducers, or effects needed | ✅ |
🎯 Type Safety | Full TypeScript support with generics | ✅ |
📱 Angular Signals | Modern reactive programming | ✅ |
🔄 RxJS Observables | Backward compatibility | ✅ |
💾 Storage Integration | localStorage/sessionStorage support | ✅ |
🏗️ Module Federation | Micro-frontend ready | ✅ |
🎨 Custom Actions | Service-based action handling | ✅ |
🔧 State Reducers | Advanced state transformations | ✅ |
📊 DevTools | NgRx DevTools integration | ✅ |
🌐 SSR Support | Server-side rendering compatible | ✅ |
Get up and running in less than 2 minutes:
# Using npm
npm install @smoosee/ngrx-manager
# Using yarn
yarn add @smoosee/ngrx-manager
# Using pnpm
pnpm add @smoosee/ngrx-manager
// app.module.ts
import { NgModule } from '@angular/core';
import { createStore } from '@smoosee/ngrx-manager';
// Create your store configuration
const [injectStore, provideStoreFor] = createStore([
{
name: 'user',
initial: { name: '', email: '', isLoggedIn: false }
},
{
name: 'cart',
initial: { items: [], total: 0 }
}
]);
@NgModule({
providers: [
provideStoreFor('root') // Use 'root' for global store
]
})
export class AppModule {}
import { Component } from '@angular/core';
@Component({
template: `
<div class="user-profile">
<h2>Welcome, {{ userState().name }}!</h2>
<p>Cart Total: ${{ cartState().total }}</p>
<button (click)="login()">Login</button>
<button (click)="addToCart()">Add Item</button>
</div>
`
})
export class UserComponent {
// Inject the store using the function from createStore
private store = injectStore();
// 🎯 Reactive state access
userState = this.store.select('user'); // Returns current value
cartState = this.store.select('cart');
// 📡 Observable access
user$ = this.store.select('user', true).asObservable();
// 📱 Signal access
userSignal = this.store.select('user', true).asSignal();
login() {
this.store.set('user', {
name: 'John Doe',
email: 'john@example.com',
isLoggedIn: true
});
}
addToCart() {
this.store.extend('cart', {
items: [...this.cartState.items, { id: Date.now(), name: 'New Item' }],
total: this.cartState.total + 29.99
});
}
}
graph TD
A[Component] --> B[StoreFacade]
B --> C[StoreManager]
B --> D[StoreDispatcher]
C --> E[NgRx Store]
D --> F[StoreEffects]
F --> G[Injectable Services]
C --> H[State Reducers]
H --> I[StorageReducer]
H --> J[MergeReducer]
E --> K[localStorage/sessionStorage]
class StoreFacade<States, Keys> {
// State Selection
select(stateKey: Keys): StateData;
select(stateKey: Keys, asReactive: true): { value, asObservable(), asSignal() };
select(stateKey: Keys, property: keyof StateData): PropertyValue;
select(stateKey: Keys, property: keyof StateData, asReactive: true): { value, asObservable(), asSignal() };
// State Mutations
set(stateKey: Keys, data: StateData): Observable<StateData>;
set(stateKey: Keys, property: keyof StateData, value: DispatchArguments): Observable<StateData>;
extend(stateKey: Keys, partialData: Partial<StateData>): Observable<StateData>;
unset(stateKey: Keys): Observable<StateData>;
// Action Dispatching
dispatch(stateKey: Keys, actionName: string, payload?: DispatchArguments): Observable<ActionResponse | StateData>;
// Action Response Listening
on(stateKey: Keys, actionName: string, status: 'PENDING' | 'SUCCESS' | 'ERROR', observe: 'state' | 'action'): Observable<ActionResponse | StateData>;
// Utility Methods
clear(exclude?: Keys[]): void;
restore(): void;
init(stateKey: Keys, getter: Observable<StateData>, formatter?: Function): Observable<StateData>;
}
Configure global store behavior:
interface StoreOptions {
app?: string; // Application namespace
prefix?: string; // Global prefix for storage keys
storage?: 'local' | 'session' | 'none'; // Storage strategy
flags?: { // Update behavior flags
onSet?: 'extend' | 'merge' | 'override' | 'replace';
onDispatch?: 'extend' | 'merge' | 'override' | 'replace';
};
}
📋 Complete StoreOptions Reference
Property | Type | Default | Description |
---|---|---|---|
app |
string |
undefined |
Groups related states under a namespace |
prefix |
string |
undefined |
Adds a prefix to all storage keys |
storage |
'local' | 'session' | 'none' |
'none' |
Persistence strategy for state data |
flags.onSet |
UpdateFlag |
'replace' |
How set() operations merge data |
flags.onDispatch |
UpdateFlag |
'extend' |
How custom actions merge data |
Update Flags:
-
'extend'
: Deep merge, preserving nested objects, merges arrays. -
'merge'
: Shallow merge, replacing nested objects, doesn't impact arrays. -
'override'
: Object spread merge. -
'replace'
: Complete replacement.
Example:
const [injectStore, provideStore] = createStore(
{
app: 'ecommerce',
prefix: 'myapp',
storage: 'local',
flags: {
onSet: 'replace',
onDispatch: 'extend'
}
},
states
);
Define your application states:
interface IStoreState<N extends string = string, M = any, S = Service, A = any[]> {
name: N; // State identifier
initial: M; // Initial state data
service?: S; // Optional service class for auto-generated actions
actions?: A; // Custom actions array
}
📋 Complete IStoreState Reference
Property | Type | Default | Description |
---|---|---|---|
name |
string |
- | Required. Unique identifier for the state |
initial |
T |
{} |
Required. Initial state value |
service |
ServiceClass |
undefined |
Service class for auto-generated actions |
actions |
StoreAction[] |
[] |
Custom actions for this state |
Note: The StoreState
class automatically adds:
- Default actions:
GET
,SET
,EXTEND
,UNSET
- Default reducers:
StoreReducer
,MergeReducer
,StorageReducer
- Auto-generated actions from service methods (if service provided)
Example:
interface UserModel {
id: number | null;
name: string;
email: string;
}
const userState: IStoreState<'user', UserModel> = {
name: 'user',
initial: { id: null, name: '', email: '' },
service: UserService, // Auto-generates actions from service methods
actions: [
new StoreAction({
name: 'CUSTOM_ACTION',
service: UserService,
method: 'customMethod'
})
]
};
Create custom actions with service integration:
class StoreAction<N extends string = string, S = Service, M = ServiceMethod<S>> {
name: N; // Action identifier
state?: string; // State name (auto-assigned)
service: ServiceClass<S>; // Injectable service class
method: M; // Method name to call
payload?: any; // Action payload
uuid?: string; // Unique action ID
status?: ActionStatus; // PENDING | SUCCESS | ERROR
flag?: UpdateFlag; // How to merge the result
}
📋 Complete StoreAction Reference
Property | Type | Description |
---|---|---|
name |
string |
Required. Action identifier (auto-generated from method if not provided) |
service |
ServiceClass |
Required. Injectable service class |
method |
string |
Required. Method name to call on service |
state |
string |
State name (automatically assigned) |
payload |
any |
Action payload (set when dispatched) |
uuid |
string |
Unique action identifier (auto-generated) |
status |
ActionStatus |
Action status: PENDING , SUCCESS , ERROR
|
flag |
UpdateFlag |
How to merge result: extend , merge , override , replace
|
Default Actions Available:
-
GET
: Retrieve current state -
SET
: Replace state data -
EXTEND
: Deep merge with current state -
UNSET
: Clear state to initial value
Example:
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
async loadUserProfile(userId: string) {
return this.http.get(`/api/users/${userId}`).toPromise();
}
updateProfile(userData: Partial<User>) {
return this.http.put('/api/user/profile', userData);
}
}
// Custom action
const loadUserAction = new StoreAction({
name: 'LOAD_USER_PROFILE',
service: UserService,
method: 'loadUserProfile'
});
// Service with auto-generated actions
const userState = {
name: 'user',
initial: {},
service: UserService // Auto-creates LOAD_USER_PROFILE and UPDATE_PROFILE actions
};
Transform state data with custom logic:
class StoreReducer {
// Get the payload to be merged with state
getPayload(state: StoreState, payload: any, action: StoreAction): any;
// Pre-process payload before state update
prePopulate(state: StoreState, payload: any, action: StoreAction): any;
// Post-process payload after state update
postPopulate(state: StoreState, payload: any, action: StoreAction): any;
// Main reducer logic (called automatically)
onPopulate(state: StoreState, payload: any, action: StoreAction): any;
}
Built-in Reducers:
-
StoreReducer
: Base reducer with action status handling -
MergeReducer
: Handles different merge strategies (extend
,merge
,override
,replace
) -
StorageReducer
: Manages localStorage/sessionStorage persistence
Perfect for single-page applications:
// app.module.ts
import { NgModule } from '@angular/core';
import { createStore } from '@smoosee/ngrx-manager';
const appStates = [
{
name: 'auth',
initial: { user: null, token: null, isAuthenticated: false }
},
{
name: 'ui',
initial: { theme: 'light', sidebarOpen: false, loading: false }
},
{
name: 'notifications',
initial: { items: [], unreadCount: 0 }
}
];
const globalStoreConfig = {
app: 'myapp',
storage: 'local'
};
// Create store with global configuration
const [injectStore, provideStoreFor] = createStore(globalStoreConfig, appStates);
@NgModule({
providers: [
provideStoreFor('root') // Provides NgRx store + effects for root
]
})
export class AppModule {}
// Export for use in components
export { injectStore };
Ideal for module federation and lazy-loaded features:
// feature.module.ts
import { NgModule } from '@angular/core';
import { createStore, StoreAction } from '@smoosee/ngrx-manager';
const featureStates = [
{
name: 'products',
initial: { items: [], filters: {}, pagination: {} },
service: ProductService, // Auto-generates actions from service methods
actions: [
new StoreAction({
name: 'LOAD_PRODUCTS',
service: ProductService,
method: 'loadProducts'
})
]
},
{
name: 'cart',
initial: { items: [], total: 0, discounts: [] }
}
];
// Create feature store
const [injectStore, provideStoreFor] = createStore(
{ app: 'ecommerce' },
featureStates
);
@NgModule({
providers: [
provideStoreFor('feature') // Feature-level providers only
]
})
export class EcommerceModule {}
// Export for use in feature components
export { injectStore };
The StoreFacade
provides multiple ways to update state:
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard'
})
export class DashboardComponent {
private store = injectStore(); // Use your store injection function
// 🎯 Built-in Actions
updateUser() {
// SET: Replace state (uses 'replace' flag by default)
this.store.set('user', {
id: 1,
name: 'John Doe',
email: 'john@example.com'
});
// SET with specific property
this.store.set('user', 'name', 'Jane Doe');
// EXTEND: Deep merge with existing state
this.store.extend('user', {
lastLogin: new Date(),
preferences: { theme: 'dark' }
});
// UNSET: Clear state to initial value
this.store.unset('user');
}
// 🔧 Custom Actions
async loadUserData() {
// Dispatch custom action - returns Observable
const result$ = this.store.dispatch('user', 'LOAD_USER_PROFILE', { userId: 123 });
// Subscribe to get the result
result$.subscribe(updatedState => {
console.log('User loaded:', updatedState);
});
}
// 🧹 Utility Actions
clearAllData() {
this.store.clear(); // Clear all states
}
// 🔄 Advanced: Clear with exclusions
clearExceptUser() {
this.store.clear(['user']); // Clear all except user state
}
// 💾 Cache and Restore
cacheAndClear() {
this.store.clear(['user']); // This caches 'user' state
// Later...
this.store.restore(); // Restores cached states
}
}
Multiple ways to consume state data:
@Component({
template: `
<!-- 📊 Direct State Access -->
<div class="user-info">
<h2>{{ userState.name }}</h2>
<p>{{ userState.email }}</p>
</div>
<!-- 🔄 Using Observables -->
<div class="user-status" *ngIf="userObservable$ | async as user">
<span [class.online]="user.isOnline">
{{ user.isOnline ? 'Online' : 'Offline' }}
</span>
</div>
<!-- 📱 Using Signals -->
<div class="user-signal">
Signal Value: {{ userSignal().name }}
</div>
<!-- 🎯 Property Access -->
<div class="user-property">
Just Name: {{ userName }}
</div>
`
})
export class UserProfileComponent {
private store = injectStore();
// 📊 Direct state access (synchronous)
userState = this.store.select('user');
cartState = this.store.select('cart');
// 🔄 Observable access
userObservable$ = this.store.select('user', true).asObservable();
cartObservable$ = this.store.select('cart', true).asObservable();
// 📱 Signal access
userSignal = this.store.select('user', true).asSignal();
cartSignal = this.store.select('cart', true).asSignal();
// 🎯 Property-specific access
userName = this.store.select('user', 'name'); // Direct property access
userNameObservable$ = this.store.select('user', 'name', true).asObservable();
userNameSignal = this.store.select('user', 'name', true).asSignal();
// 🎨 Computed Values
get isUserLoggedIn() {
return this.userState.isAuthenticated;
}
get cartItemCount() {
return this.cartState.items?.length || 0;
}
// 🔄 Using init for lazy loading
ngOnInit() {
// Initialize state with data from service
this.store.init('user', this.userService.getCurrentUser(), (data) => ({
...data,
lastAccessed: new Date()
})).subscribe();
}
// 👂 Listen for specific action responses
listenToUserActions() {
// Listen to all responses for a specific action
this.store.on('user', 'LOAD_USER_PROFILE').subscribe(response => {
console.log('Action:', response.action);
console.log('Status:', response.status); // PENDING, SUCCESS, or ERROR
console.log('Updated State:', response.state);
console.log('Raw Payload:', response.payload);
});
// Listen only for errors
this.store.on('user', 'LOAD_USER_PROFILE', 'ERROR').subscribe(response => {
console.error('Failed to load user profile:', response.payload);
});
}
}
Listen to specific action responses and handle different states:
@Component({
selector: 'app-user-actions'
})
export class UserActionsComponent {
private store = injectStore();
ngOnInit() {
this.setupActionListeners();
}
setupActionListeners() {
// 🎯 Listen only to successful responses
this.store.on('user', 'UPDATE_PROFILE').subscribe(response => {
this.showNotification('Profile updated successfully!');
// Access the updated state
console.log('Updated user:', response.state);
});
// 🎯 Listen only to errors
this.store.on('user', 'UPDATE_PROFILE', 'ERROR').subscribe(response => {
this.showErrorDialog('Update failed', response.payload.message);
});
// 🎯 Listen to pending actions for loading states
this.store.on('products', 'LOAD_PRODUCTS', 'PENDING').subscribe(() => {
this.isLoading = true;
});
}
async loadUserProfile(userId: number) {
// Dispatch action and also listen for its response
this.store.dispatch('user', 'LOAD_USER_PROFILE', { userId }).subscribe();
}
// Helper methods
private showLoadingSpinner() { /* ... */ }
private hideLoadingSpinner() { /* ... */ }
private showSuccessMessage(message: string) { /* ... */ }
private showErrorMessage(message: string) { /* ... */ }
private showNotification(message: string) { /* ... */ }
private showErrorDialog(title: string, message: string) { /* ... */ }
}
-
Loading States: Listen to
PENDING
status to show loading indicators -
Success Handling: Listen to
SUCCESS
status to show notifications or navigate -
Error Handling: Listen to
ERROR
status to display error messages - State Synchronization: Access the updated state immediately after actions complete
- Audit Logging: Track all action executions and their outcomes
Create fully typed stores for maximum developer experience:
// 📝 Step 1: Define State Interfaces
interface UserState {
id: number | null;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
};
isAuthenticated: boolean;
}
interface CartState {
items: CartItem[];
total: number;
currency: string;
discounts: Discount[];
}
interface UIState {
sidebarOpen: boolean;
loading: boolean;
currentPage: string;
breadcrumbs: BreadcrumbItem[];
}
// 🏗️ Step 2: Create Typed Store Configuration
export const AppStoreStates = [
new StoreState({
name: 'user',
initial: <UserState>{
id: null,
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true,
language: 'en'
},
isAuthenticated: false
},
actions: [
new StoreAction({
name: 'LOAD_USER_PROFILE',
service: UserService,
method: 'loadProfile'
}),
new StoreAction({
name: 'UPDATE_PREFERENCES',
service: UserService,
method: 'updatePreferences'
})
]
}),
new StoreState({
name: 'cart',
initial: <CartState>{
items: [],
total: 0,
currency: 'USD',
discounts: []
},
actions: [
new StoreAction({
name: 'ADD_TO_CART',
service: CartService,
method: 'addItem'
}),
new StoreAction({
name: 'CALCULATE_TOTAL',
service: CartService,
method: 'calculateTotal'
})
]
}),
new StoreState({
name: 'ui',
initial: <UIState>{
sidebarOpen: false,
loading: false,
currentPage: 'home',
breadcrumbs: []
}
})
] as const;
// 🎯 Step 3: Create Typed Store
const [injectAppStore, provideAppStore] = createStore(AppStoreStates);
// 🎨 Create a typed facade service (optional)
@Injectable({ providedIn: 'root' })
export class AppStoreFacade {
private store = injectAppStore();
// 🎯 Typed state selectors
user = this.store.select('user');
cart = this.store.select('cart');
ui = this.store.select('ui');
// 🔄 Typed observables
user$ = this.store.select('user', true).asObservable();
cart$ = this.store.select('cart', true).asObservable();
ui$ = this.store.select('ui', true).asObservable();
// 📱 Typed signals
userSignal = this.store.select('user', true).asSignal();
cartSignal = this.store.select('cart', true).asSignal();
// 🎯 Typed action dispatchers
loadUserProfile(userId: number) {
return this.store.dispatch('user', 'LOAD_USER_PROFILE', { userId });
}
addToCart(item: CartItem) {
return this.store.dispatch('cart', 'ADD_TO_CART', item);
}
toggleSidebar() {
const currentState = this.store.select('ui');
this.store.extend('ui', { sidebarOpen: !currentState.sidebarOpen });
}
}
Complete E-commerce Store Setup
// models/ecommerce.models.ts
export interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
images: string[];
}
export interface CartItem extends Product {
quantity: number;
}
export interface User {
id: string;
email: string;
firstName: string;
lastName: string;
addresses: Address[];
}
// store/ecommerce.store.ts
export const EcommerceStoreStates = [
new StoreState({
name: 'products',
initial: {
items: [] as Product[],
categories: [] as string[],
filters: {
category: '',
priceRange: [0, 1000],
inStock: true
},
pagination: {
page: 1,
limit: 20,
total: 0
},
loading: false
},
actions: [
new StoreAction({
name: 'LOAD_PRODUCTS',
service: ProductService,
method: 'loadProducts'
}),
new StoreAction({
name: 'FILTER_PRODUCTS',
service: ProductService,
method: 'filterProducts'
})
]
}),
new StoreState({
name: 'cart',
initial: {
items: [] as CartItem[],
total: 0,
subtotal: 0,
tax: 0,
shipping: 0,
discounts: [],
couponCode: ''
},
actions: [
new StoreAction({
name: 'ADD_TO_CART',
service: CartService,
method: 'addItem'
}),
new StoreAction({
name: 'UPDATE_QUANTITY',
service: CartService,
method: 'updateQuantity'
}),
new StoreAction({
name: 'APPLY_COUPON',
service: CartService,
method: 'applyCoupon'
})
],
options: { storage: 'local' }
}),
new StoreState({
name: 'user',
initial: {
profile: null as User | null,
isAuthenticated: false,
preferences: {
currency: 'USD',
language: 'en'
}
},
actions: [
new StoreAction({
name: 'LOGIN',
service: AuthService,
method: 'login'
}),
new StoreAction({
name: 'LOAD_PROFILE',
service: UserService,
method: 'loadProfile'
})
],
options: { storage: 'session' }
})
] as const;
// components/product-list.component.ts
@Component({
template: `
<div class="product-grid">
<div class="filters">
<select (change)="filterByCategory($event)">
<option value="">All Categories</option>
<option *ngFor="let category of productsState().categories"
[value]="category">
{{ category }}
</option>
</select>
</div>
<div class="products" *ngIf="!productsState().loading">
<div *ngFor="let product of productsState().items"
class="product-card">
<img [src]="product.images[0]" [alt]="product.name">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<button (click)="addToCart(product)"
[disabled]="!product.inStock">
Add to Cart
</button>
</div>
</div>
<div *ngIf="productsState().loading" class="loading">
Loading products...
</div>
</div>
`
})
export class ProductListComponent {
private store = inject(StoreFacade);
productsState = this.store.select('products', false);
cartState = this.store.select('cart', false);
async ngOnInit() {
await this.store.dispatch('products', 'LOAD_PRODUCTS');
}
async filterByCategory(event: Event) {
const category = (event.target as HTMLSelectElement).value;
await this.store.dispatch('products', 'FILTER_PRODUCTS', { category });
}
async addToCart(product: Product) {
await this.store.dispatch('cart', 'ADD_TO_CART', {
...product,
quantity: 1
});
}
}
Social Media Dashboard Store
// store/social.store.ts
export const SocialStoreStates = [
new StoreState({
name: 'posts',
initial: {
feed: [] as Post[],
userPosts: [] as Post[],
drafts: [] as Draft[],
loading: false,
pagination: {
hasMore: true,
cursor: null
}
},
actions: [
new StoreAction({
name: 'LOAD_FEED',
service: PostService,
method: 'loadFeed'
}),
new StoreAction({
name: 'CREATE_POST',
service: PostService,
method: 'createPost'
}),
new StoreAction({
name: 'LIKE_POST',
service: PostService,
method: 'likePost'
})
]
}),
new StoreState({
name: 'notifications',
initial: {
items: [] as Notification[],
unreadCount: 0,
settings: {
email: true,
push: true,
mentions: true,
likes: false
}
},
actions: [
new StoreAction({
name: 'LOAD_NOTIFICATIONS',
service: NotificationService,
method: 'loadNotifications'
}),
new StoreAction({
name: 'MARK_AS_READ',
service: NotificationService,
method: 'markAsRead'
})
]
}),
new StoreState({
name: 'chat',
initial: {
conversations: [] as Conversation[],
activeConversation: null as string | null,
messages: {} as Record<string, Message[]>,
onlineUsers: [] as string[]
},
actions: [
new StoreAction({
name: 'LOAD_CONVERSATIONS',
service: ChatService,
method: 'loadConversations'
}),
new StoreAction({
name: 'SEND_MESSAGE',
service: ChatService,
method: 'sendMessage'
})
]
})
] as const;
Transform state data with custom logic:
const userStateReducer: StateReducer = {
mapReduce: (state, value, action) => {
if (action?.name === 'LOGIN') {
return {
...value,
lastLogin: new Date(),
sessionId: generateSessionId()
};
}
return value;
}
};
const userState = new StoreState({
name: 'user',
initial: { /* ... */ },
reducers: [userStateReducer]
});
// Global persistence
StoreModule.forRoot(
{ storage: 'local' }, // All states persist to localStorage
states
);
// Per-state persistence
const states = [
{
name: 'user',
initial: { /* ... */ },
options: { storage: 'session' } // User data in sessionStorage
},
{
name: 'cart',
initial: { /* ... */ },
options: { storage: 'local' } // Cart persists across sessions
},
{
name: 'ui',
initial: { /* ... */ },
options: { storage: 'none' } // UI state is ephemeral
}
];
// Shell application
@NgModule({
providers: [
provideStoreFor('root')
]
})
export class ShellModule {}
// Micro-frontend module
@NgModule({
providers: [
provideStoreFor('feature')
]
})
export class FeatureAModule {}
describe('UserComponent', () => {
let component: UserComponent;
let mockStore: jasmine.SpyObj<any>;
beforeEach(() => {
// Create a mock store that matches the actual API
mockStore = jasmine.createSpyObj('Store', [
'select', 'set', 'extend', 'dispatch', 'unset', 'clear', 'restore', 'init'
]);
// Mock the select method to return test data
mockStore.select.and.returnValue({ name: 'Test User', email: 'test@example.com' });
TestBed.configureTestingModule({
declarations: [UserComponent],
providers: [
// Mock the store injection function
{ provide: 'STORE_INJECTION_TOKEN', useValue: () => mockStore }
]
});
component = TestBed.createComponent(UserComponent).componentInstance;
});
it('should display user name', () => {
expect(component.userState.name).toBe('Test User');
});
it('should call set when updating user', () => {
component.updateUser();
expect(mockStore.set).toHaveBeenCalledWith('user', jasmine.any(Object));
});
});
describe('Store Integration', () => {
let injectStore: () => any;
let store: any;
beforeEach(() => {
// Create test store
const [inject, provide] = createStore([
{ name: 'test', initial: { count: 0 } }
]);
TestBed.configureTestingModule({
providers: [
...provide('root')
]
});
injectStore = inject;
store = injectStore();
});
it('should update state correctly', () => {
store.set('test', { count: 5 }).subscribe();
expect(store.select('test')).toEqual(jasmine.objectContaining({ count: 5 }));
});
it('should extend state correctly', () => {
store.set('test', { count: 5, name: 'test' }).subscribe();
store.extend('test', { count: 10 }).subscribe();
const state = store.select('test');
expect(state.count).toBe(10);
expect(state.name).toBe('test');
});
});
// ✅ Good: Use signals for reactive updates
const userSignal = this.store.select('user', false);
// ✅ Good: Use observables for complex async operations
const user$ = this.store.select('user', true).pipe(
debounceTime(300),
distinctUntilChanged()
);
// ❌ Avoid: Frequent synchronous calls in templates
// {{ store.select('user').name }} // Don't do this
// ✅ Use tree-shakable imports
import { StoreFacade } from '@smoosee/ngrx-manager';
// ✅ Lazy load feature stores
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule)
}
];
We love contributions! Here's how you can help:
- Check existing issues
- Create a detailed bug report
- Include reproduction steps
- Open a discussion
- Describe your use case
- Get community feedback
- Fork the repository
- Create a feature branch
- Write tests for your changes
- Submit a pull request
MIT License © 2023 Mostafa Sherif
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.