Angular CLI schematics for generating NgRx state management code including actions, reducers, effects, selectors, and feature modules.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
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;
}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 --flatShows 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();
}
}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';
}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))
)
)
)
);
}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();
});
});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';
}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';
}