CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ngrx--schematics

Angular CLI schematics for generating NgRx state management code including actions, reducers, effects, selectors, and feature modules.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

component-store.mddocs/

Component Store

NgRx component store generation schematic that creates standalone component stores using @ngrx/component-store for local component state management. Component stores provide reactive state management at the component level without affecting global application state.

Capabilities

Component Store Schematic

Generates standalone component stores with reactive state management for individual components.

# Basic component store
ng generate @ngrx/schematics:component-store UserStore

# Component store with custom path
ng generate @ngrx/schematics:component-store ProductStore --path=src/app/catalog

# Flat component store
ng generate @ngrx/schematics:component-store OrderStore --flat
/**
 * Component store schematic configuration interface
 */
interface ComponentStoreSchema {
  /** Name of the component store */
  name: string;
  /** Path where store files should be generated */
  path?: string;
  /** Angular project to target */
  project?: string;
  /** Generate files without creating a folder */
  flat?: boolean;
}

Generated Component Store

Creates a complete ComponentStore class with state management capabilities:

// Generated component store
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable, tap, switchMap, catchError } from 'rxjs';
import { of } from 'rxjs';

export interface UserState {
  users: User[];
  loading: boolean;
  error: string | null;
  selectedUserId: string | null;
  filter: string;
}

const initialState: UserState = {
  users: [],
  loading: false,
  error: null,
  selectedUserId: null,
  filter: ''
};

@Injectable()
export class UserStore extends ComponentStore<UserState> {
  
  constructor(private userService: UserService) {
    super(initialState);
  }

  // Selectors
  readonly users$ = this.select(state => state.users);
  readonly loading$ = this.select(state => state.loading);
  readonly error$ = this.select(state => state.error);
  readonly selectedUserId$ = this.select(state => state.selectedUserId);
  readonly filter$ = this.select(state => state.filter);

  // Derived selectors
  readonly selectedUser$ = this.select(
    this.users$,
    this.selectedUserId$,
    (users, selectedId) => users.find(user => user.id === selectedId) || null
  );

  readonly filteredUsers$ = this.select(
    this.users$,
    this.filter$,
    (users, filter) => 
      filter 
        ? users.filter(user => 
            user.name.toLowerCase().includes(filter.toLowerCase()) ||
            user.email.toLowerCase().includes(filter.toLowerCase())
          )
        : users
  );

  readonly userCount$ = this.select(this.users$, users => users.length);

  readonly viewModel$ = this.select(
    this.filteredUsers$,
    this.loading$,
    this.error$,
    this.selectedUser$,
    (users, loading, error, selectedUser) => ({
      users,
      loading,
      error,
      selectedUser,
      hasUsers: users.length > 0,
      hasSelection: selectedUser !== null
    })
  );

  // Updaters
  readonly setLoading = this.updater((state, loading: boolean) => ({
    ...state,
    loading
  }));

  readonly setError = this.updater((state, error: string | null) => ({
    ...state,
    error,
    loading: false
  }));

  readonly setUsers = this.updater((state, users: User[]) => ({
    ...state,
    users,
    loading: false,
    error: null
  }));

  readonly addUser = this.updater((state, user: User) => ({
    ...state,
    users: [...state.users, user]
  }));

  readonly updateUser = this.updater((state, updatedUser: User) => ({
    ...state,
    users: state.users.map(user => 
      user.id === updatedUser.id ? updatedUser : user
    )
  }));

  readonly removeUser = this.updater((state, userId: string) => ({
    ...state,
    users: state.users.filter(user => user.id !== userId),
    selectedUserId: state.selectedUserId === userId ? null : state.selectedUserId
  }));

  readonly selectUser = this.updater((state, userId: string | null) => ({
    ...state,
    selectedUserId: userId
  }));

  readonly setFilter = this.updater((state, filter: string) => ({
    ...state,
    filter
  }));

  readonly clearError = this.updater((state) => ({
    ...state,
    error: null
  }));

  // Effects
  readonly loadUsers = this.effect<void>(trigger$ =>
    trigger$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(() =>
        this.userService.getUsers().pipe(
          tap({
            next: users => this.setUsers(users),
            error: error => this.setError(error.message || 'Failed to load users')
          }),
          catchError(() => of([]))
        )
      )
    )
  );

  readonly createUser = this.effect<User>(user$ =>
    user$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(user =>
        this.userService.createUser(user).pipe(
          tap({
            next: createdUser => {
              this.addUser(createdUser);
              this.setLoading(false);
            },
            error: error => this.setError(error.message || 'Failed to create user')
          }),
          catchError(() => of(null))
        )
      )
    )
  );

  readonly updateUserEffect = this.effect<User>(user$ =>
    user$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(user =>
        this.userService.updateUser(user).pipe(
          tap({
            next: updatedUser => {
              this.updateUser(updatedUser);
              this.setLoading(false);
            },
            error: error => this.setError(error.message || 'Failed to update user')
          }),
          catchError(() => of(null))
        )
      )
    )
  );

  readonly deleteUser = this.effect<string>(userId$ =>
    userId$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(userId =>
        this.userService.deleteUser(userId).pipe(
          tap({
            next: () => {
              this.removeUser(userId);
              this.setLoading(false);
            },
            error: error => this.setError(error.message || 'Failed to delete user')
          }),
          catchError(() => of(null))
        )
      )
    )
  );
}

Usage Examples:

# Generate user store
ng generate @ngrx/schematics:component-store UserStore

# Generate product store with custom path
ng generate @ngrx/schematics:component-store ProductStore --path=src/app/catalog

# Generate order store in flat structure
ng generate @ngrx/schematics:component-store OrderStore --flat

Component Integration

Shows how to integrate the component store with Angular components:

// Component using the store
@Component({
  selector: 'app-user-management',
  templateUrl: './user-management.component.html',
  providers: [UserStore] // Provide store at component level
})
export class UserManagementComponent implements OnInit {
  
  // Subscribe to view model for template
  readonly viewModel$ = this.userStore.viewModel$;
  
  // Individual observables if needed
  readonly users$ = this.userStore.users$;
  readonly loading$ = this.userStore.loading$;
  readonly error$ = this.userStore.error$;

  constructor(private userStore: UserStore) {}

  ngOnInit(): void {
    // Load users on component initialization
    this.userStore.loadUsers();
  }

  onCreateUser(user: User): void {
    this.userStore.createUser(user);
  }

  onUpdateUser(user: User): void {
    this.userStore.updateUserEffect(user);
  }

  onDeleteUser(userId: string): void {
    this.userStore.deleteUser(userId);
  }

  onSelectUser(userId: string): void {
    this.userStore.selectUser(userId);
  }

  onFilterChange(filter: string): void {
    this.userStore.setFilter(filter);
  }

  onClearError(): void {
    this.userStore.clearError();
  }
}

Component Store Patterns

The generated component store includes common reactive patterns:

/**
 * Component store reactive patterns
 */
interface ComponentStorePatterns {
  /** Selector pattern */
  selector: 'readonly property$ = this.select(state => state.property)';
  /** Derived selector pattern */
  derivedSelector: 'this.select(selector1, selector2, (val1, val2) => computation)';
  /** Updater pattern */
  updater: 'readonly updateMethod = this.updater((state, payload) => newState)';
  /** Effect pattern */
  effect: 'readonly effectMethod = this.effect(trigger$ => trigger$.pipe(...))';
  /** View model pattern */
  viewModel: 'Combine multiple selectors into single view model';
}

Advanced Component Store Features

Generated stores can include advanced features:

// Advanced component store with debouncing and caching
@Injectable()
export class AdvancedUserStore extends ComponentStore<UserState> {
  
  // Debounced search effect
  readonly searchUsers = this.effect<string>(searchTerm$ =>
    searchTerm$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      tap(() => this.setLoading(true)),
      switchMap(term =>
        term.length < 2 
          ? of([])
          : this.userService.searchUsers(term).pipe(
              tap({
                next: users => this.setUsers(users),
                error: error => this.setError(error.message)
              }),
              catchError(() => of([]))
            )
      )
    )
  );

  // Optimistic updates
  readonly updateUserOptimistic = this.effect<User>(user$ =>
    user$.pipe(
      tap(user => {
        // Optimistically update the UI
        this.updateUser(user);
      }),
      switchMap(user =>
        this.userService.updateUser(user).pipe(
          tap({
            next: updatedUser => {
              // Confirm the update with server response
              this.updateUser(updatedUser);
            },
            error: error => {
              // Revert optimistic update on error
              this.loadUsers();
              this.setError(error.message);
            }
          }),
          catchError(() => of(null))
        )
      )
    )
  );

  // Pagination support
  readonly loadPage = this.effect<{ page: number; pageSize: number }>(
    pageInfo$ => pageInfo$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(({ page, pageSize }) =>
        this.userService.getUsersPage(page, pageSize).pipe(
          tap({
            next: result => {
              this.patchState({
                users: result.data,
                currentPage: page,
                totalPages: result.totalPages,
                loading: false
              });
            },
            error: error => this.setError(error.message)
          }),
          catchError(() => of(null))
        )
      )
    )
  );
}

Component Store Testing

Generated component stores include comprehensive testing setup:

// Component store testing
describe('UserStore', () => {
  let store: UserStore;
  let userService: jasmine.SpyObj<UserService>;

  beforeEach(() => {
    const spy = jasmine.createSpyObj('UserService', [
      'getUsers',
      'createUser',
      'updateUser',
      'deleteUser'
    ]);

    TestBed.configureTestingModule({
      providers: [
        UserStore,
        { provide: UserService, useValue: spy }
      ]
    });

    store = TestBed.inject(UserStore);
    userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });

  it('should initialize with default state', (done) => {
    store.state$.subscribe(state => {
      expect(state.users).toEqual([]);
      expect(state.loading).toBeFalse();
      expect(state.error).toBeNull();
      done();
    });
  });

  it('should load users', (done) => {
    const users = [{ id: '1', name: 'John', email: 'john@example.com' }];
    userService.getUsers.and.returnValue(of(users));

    store.users$.subscribe(result => {
      if (result.length > 0) {
        expect(result).toEqual(users);
        done();
      }
    });

    store.loadUsers();
  });

  it('should add user', (done) => {
    const newUser = { id: '1', name: 'John', email: 'john@example.com' };
    
    store.addUser(newUser);
    
    store.users$.subscribe(users => {
      if (users.length > 0) {
        expect(users).toContain(newUser);
        done();
      }
    });
  });

  it('should update user', (done) => {
    const user = { id: '1', name: 'John', email: 'john@example.com' };
    const updatedUser = { ...user, name: 'John Updated' };
    
    store.addUser(user);
    store.updateUser(updatedUser);
    
    store.users$.subscribe(users => {
      const foundUser = users.find(u => u.id === '1');
      if (foundUser && foundUser.name === 'John Updated') {
        expect(foundUser.name).toBe('John Updated');
        done();
      }
    });
  });

  it('should handle errors', (done) => {
    const errorMessage = 'Network error';
    userService.getUsers.and.returnValue(throwError({ message: errorMessage }));

    store.error$.subscribe(error => {
      if (error) {
        expect(error).toBe(errorMessage);
        done();
      }
    });

    store.loadUsers();
  });
});

Component Store vs Global Store

Comparison of when to use component store vs global NgRx store:

/**
 * Component Store use cases
 */
interface ComponentStoreUseCases {
  /** Local component state */
  localState: 'State specific to single component/feature';
  /** Temporary data */
  temporaryData: 'Data that doesn\'t need to persist across routes';
  /** Form state */
  formState: 'Complex form state management';
  /** UI state */
  uiState: 'Modal state, filters, pagination';
  /** Isolated features */
  isolatedFeatures: 'Self-contained features with own lifecycle';
}

/**
 * Global Store use cases
 */
interface GlobalStoreUseCases {
  /** Shared state */
  sharedState: 'State shared across multiple components';
  /** Persistent data */
  persistentData: 'Data that needs to survive route changes';
  /** User authentication */
  authentication: 'User session and auth state';
  /** Application configuration */
  configuration: 'Global app settings and config';
  /** Complex workflows */
  complexWorkflows: 'Multi-step processes across components';
}

Performance Benefits

Component stores provide several performance advantages:

/**
 * Component store performance benefits
 */
interface PerformanceBenefits {
  /** Scoped state */
  scopedState: 'State lifecycle tied to component lifecycle';
  /** Automatic cleanup */
  automaticCleanup: 'State cleaned up when component destroyed';
  /** Reduced global state */
  reducedGlobalState: 'Less pollution of global state tree';
  /** Reactive updates */
  reactiveUpdates: 'Efficient reactive state updates';
  /** OnPush compatibility */
  onPushCompatible: 'Works seamlessly with OnPush change detection';
}

docs

action-generation.md

component-store.md

container-components.md

data-services.md

effect-generation.md

entity-management.md

feature-generation.md

index.md

ngrx-push-migration.md

reducer-generation.md

selector-generation.md

store-setup.md

utility-functions.md

tile.json