@smoosee/ngrx-manager
TypeScript icon, indicating that this package has built-in type declarations

20.0.1 • Public • Published

🚀 @smoosee/ngrx-manager

The Ultimate Plug-N-Play State Manager for NgRx

Simplify your Angular state management with zero boilerplate and maximum productivity

Quick StartFeaturesDocumentationExamplesCommunity


✨ Features

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

🚀 Quick Start

Get up and running in less than 2 minutes:

📦 Installation

# Using npm
npm install @smoosee/ngrx-manager

# Using yarn
yarn add @smoosee/ngrx-manager

# Using pnpm
pnpm add @smoosee/ngrx-manager

⚡ Basic Setup

// 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 {}

🎯 Usage in Components

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
    });
  }
}

📚 Comprehensive Documentation

🏗️ Architecture Overview

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]

🎯 Core API Reference

StoreFacade Methods

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>;
}

🔧 Configuration Types

StoreOptions

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
);

IStoreState

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'
    })
  ]
};

StoreAction

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
};

StoreReducer

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

🏗️ Store Setup Patterns

🌍 Global Store Setup

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 };

🧩 Feature Store Setup

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 };

🎯 State Management Patterns

📡 Dispatching Actions

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
  }
}

👂 Listening to State Changes

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);
    });
  }
}

👂 Action Response Listening

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) { /* ... */ }
}

Use Cases

  • 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

💪 Advanced Type Safety

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 });
  }
}

🎨 Real-World Examples

🛒 E-commerce Application

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

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;

🔧 Advanced Features

🎨 Custom State Reducers

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]
});

🔄 State Persistence Strategies

// 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
  }
];

🏗️ Module Federation Support

// Shell application
@NgModule({
  providers: [
    provideStoreFor('root')
  ]
})
export class ShellModule {}

// Micro-frontend module
@NgModule({
  providers: [
    provideStoreFor('feature')
  ]
})
export class FeatureAModule {}

🧪 Testing

Unit Testing Components

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));
  });
});

Integration Testing

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');
  });
});

🚀 Performance Tips

🎯 Optimize State Selection

// ✅ 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

📦 Bundle Size Optimization

// ✅ 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)
  }
];

🤝 Contributing

We love contributions! Here's how you can help:

🐛 Found a Bug?

  1. Check existing issues
  2. Create a detailed bug report
  3. Include reproduction steps

💡 Have an Idea?

  1. Open a discussion
  2. Describe your use case
  3. Get community feedback

🔧 Want to Code?

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for your changes
  4. Submit a pull request

📄 License

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.


🌟 Star us on GitHub!

If NgRx Manager helped you build amazing Angular applications, please consider giving us a star ⭐

GitHub stars

Made with ❤️ by developers, for developers

Package Sidebar

Install

npm i @smoosee/ngrx-manager

Weekly Downloads

16

Version

20.0.1

License

MIT

Unpacked Size

132 kB

Total Files

7

Last publish

Collaborators

  • mosherif87