0
# Component Store
1
2
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.
3
4
## Capabilities
5
6
### Component Store Schematic
7
8
Generates standalone component stores with reactive state management for individual components.
9
10
```bash
11
# Basic component store
12
ng generate @ngrx/schematics:component-store UserStore
13
14
# Component store with custom path
15
ng generate @ngrx/schematics:component-store ProductStore --path=src/app/catalog
16
17
# Flat component store
18
ng generate @ngrx/schematics:component-store OrderStore --flat
19
```
20
21
```typescript { .api }
22
/**
23
* Component store schematic configuration interface
24
*/
25
interface ComponentStoreSchema {
26
/** Name of the component store */
27
name: string;
28
/** Path where store files should be generated */
29
path?: string;
30
/** Angular project to target */
31
project?: string;
32
/** Generate files without creating a folder */
33
flat?: boolean;
34
}
35
```
36
37
### Generated Component Store
38
39
Creates a complete ComponentStore class with state management capabilities:
40
41
```typescript
42
// Generated component store
43
import { Injectable } from '@angular/core';
44
import { ComponentStore } from '@ngrx/component-store';
45
import { Observable, tap, switchMap, catchError } from 'rxjs';
46
import { of } from 'rxjs';
47
48
export interface UserState {
49
users: User[];
50
loading: boolean;
51
error: string | null;
52
selectedUserId: string | null;
53
filter: string;
54
}
55
56
const initialState: UserState = {
57
users: [],
58
loading: false,
59
error: null,
60
selectedUserId: null,
61
filter: ''
62
};
63
64
@Injectable()
65
export class UserStore extends ComponentStore<UserState> {
66
67
constructor(private userService: UserService) {
68
super(initialState);
69
}
70
71
// Selectors
72
readonly users$ = this.select(state => state.users);
73
readonly loading$ = this.select(state => state.loading);
74
readonly error$ = this.select(state => state.error);
75
readonly selectedUserId$ = this.select(state => state.selectedUserId);
76
readonly filter$ = this.select(state => state.filter);
77
78
// Derived selectors
79
readonly selectedUser$ = this.select(
80
this.users$,
81
this.selectedUserId$,
82
(users, selectedId) => users.find(user => user.id === selectedId) || null
83
);
84
85
readonly filteredUsers$ = this.select(
86
this.users$,
87
this.filter$,
88
(users, filter) =>
89
filter
90
? users.filter(user =>
91
user.name.toLowerCase().includes(filter.toLowerCase()) ||
92
user.email.toLowerCase().includes(filter.toLowerCase())
93
)
94
: users
95
);
96
97
readonly userCount$ = this.select(this.users$, users => users.length);
98
99
readonly viewModel$ = this.select(
100
this.filteredUsers$,
101
this.loading$,
102
this.error$,
103
this.selectedUser$,
104
(users, loading, error, selectedUser) => ({
105
users,
106
loading,
107
error,
108
selectedUser,
109
hasUsers: users.length > 0,
110
hasSelection: selectedUser !== null
111
})
112
);
113
114
// Updaters
115
readonly setLoading = this.updater((state, loading: boolean) => ({
116
...state,
117
loading
118
}));
119
120
readonly setError = this.updater((state, error: string | null) => ({
121
...state,
122
error,
123
loading: false
124
}));
125
126
readonly setUsers = this.updater((state, users: User[]) => ({
127
...state,
128
users,
129
loading: false,
130
error: null
131
}));
132
133
readonly addUser = this.updater((state, user: User) => ({
134
...state,
135
users: [...state.users, user]
136
}));
137
138
readonly updateUser = this.updater((state, updatedUser: User) => ({
139
...state,
140
users: state.users.map(user =>
141
user.id === updatedUser.id ? updatedUser : user
142
)
143
}));
144
145
readonly removeUser = this.updater((state, userId: string) => ({
146
...state,
147
users: state.users.filter(user => user.id !== userId),
148
selectedUserId: state.selectedUserId === userId ? null : state.selectedUserId
149
}));
150
151
readonly selectUser = this.updater((state, userId: string | null) => ({
152
...state,
153
selectedUserId: userId
154
}));
155
156
readonly setFilter = this.updater((state, filter: string) => ({
157
...state,
158
filter
159
}));
160
161
readonly clearError = this.updater((state) => ({
162
...state,
163
error: null
164
}));
165
166
// Effects
167
readonly loadUsers = this.effect<void>(trigger$ =>
168
trigger$.pipe(
169
tap(() => this.setLoading(true)),
170
switchMap(() =>
171
this.userService.getUsers().pipe(
172
tap({
173
next: users => this.setUsers(users),
174
error: error => this.setError(error.message || 'Failed to load users')
175
}),
176
catchError(() => of([]))
177
)
178
)
179
)
180
);
181
182
readonly createUser = this.effect<User>(user$ =>
183
user$.pipe(
184
tap(() => this.setLoading(true)),
185
switchMap(user =>
186
this.userService.createUser(user).pipe(
187
tap({
188
next: createdUser => {
189
this.addUser(createdUser);
190
this.setLoading(false);
191
},
192
error: error => this.setError(error.message || 'Failed to create user')
193
}),
194
catchError(() => of(null))
195
)
196
)
197
)
198
);
199
200
readonly updateUserEffect = this.effect<User>(user$ =>
201
user$.pipe(
202
tap(() => this.setLoading(true)),
203
switchMap(user =>
204
this.userService.updateUser(user).pipe(
205
tap({
206
next: updatedUser => {
207
this.updateUser(updatedUser);
208
this.setLoading(false);
209
},
210
error: error => this.setError(error.message || 'Failed to update user')
211
}),
212
catchError(() => of(null))
213
)
214
)
215
)
216
);
217
218
readonly deleteUser = this.effect<string>(userId$ =>
219
userId$.pipe(
220
tap(() => this.setLoading(true)),
221
switchMap(userId =>
222
this.userService.deleteUser(userId).pipe(
223
tap({
224
next: () => {
225
this.removeUser(userId);
226
this.setLoading(false);
227
},
228
error: error => this.setError(error.message || 'Failed to delete user')
229
}),
230
catchError(() => of(null))
231
)
232
)
233
)
234
);
235
}
236
```
237
238
**Usage Examples:**
239
240
```bash
241
# Generate user store
242
ng generate @ngrx/schematics:component-store UserStore
243
244
# Generate product store with custom path
245
ng generate @ngrx/schematics:component-store ProductStore --path=src/app/catalog
246
247
# Generate order store in flat structure
248
ng generate @ngrx/schematics:component-store OrderStore --flat
249
```
250
251
### Component Integration
252
253
Shows how to integrate the component store with Angular components:
254
255
```typescript
256
// Component using the store
257
@Component({
258
selector: 'app-user-management',
259
templateUrl: './user-management.component.html',
260
providers: [UserStore] // Provide store at component level
261
})
262
export class UserManagementComponent implements OnInit {
263
264
// Subscribe to view model for template
265
readonly viewModel$ = this.userStore.viewModel$;
266
267
// Individual observables if needed
268
readonly users$ = this.userStore.users$;
269
readonly loading$ = this.userStore.loading$;
270
readonly error$ = this.userStore.error$;
271
272
constructor(private userStore: UserStore) {}
273
274
ngOnInit(): void {
275
// Load users on component initialization
276
this.userStore.loadUsers();
277
}
278
279
onCreateUser(user: User): void {
280
this.userStore.createUser(user);
281
}
282
283
onUpdateUser(user: User): void {
284
this.userStore.updateUserEffect(user);
285
}
286
287
onDeleteUser(userId: string): void {
288
this.userStore.deleteUser(userId);
289
}
290
291
onSelectUser(userId: string): void {
292
this.userStore.selectUser(userId);
293
}
294
295
onFilterChange(filter: string): void {
296
this.userStore.setFilter(filter);
297
}
298
299
onClearError(): void {
300
this.userStore.clearError();
301
}
302
}
303
```
304
305
### Component Store Patterns
306
307
The generated component store includes common reactive patterns:
308
309
```typescript { .api }
310
/**
311
* Component store reactive patterns
312
*/
313
interface ComponentStorePatterns {
314
/** Selector pattern */
315
selector: 'readonly property$ = this.select(state => state.property)';
316
/** Derived selector pattern */
317
derivedSelector: 'this.select(selector1, selector2, (val1, val2) => computation)';
318
/** Updater pattern */
319
updater: 'readonly updateMethod = this.updater((state, payload) => newState)';
320
/** Effect pattern */
321
effect: 'readonly effectMethod = this.effect(trigger$ => trigger$.pipe(...))';
322
/** View model pattern */
323
viewModel: 'Combine multiple selectors into single view model';
324
}
325
```
326
327
### Advanced Component Store Features
328
329
Generated stores can include advanced features:
330
331
```typescript
332
// Advanced component store with debouncing and caching
333
@Injectable()
334
export class AdvancedUserStore extends ComponentStore<UserState> {
335
336
// Debounced search effect
337
readonly searchUsers = this.effect<string>(searchTerm$ =>
338
searchTerm$.pipe(
339
debounceTime(300),
340
distinctUntilChanged(),
341
tap(() => this.setLoading(true)),
342
switchMap(term =>
343
term.length < 2
344
? of([])
345
: this.userService.searchUsers(term).pipe(
346
tap({
347
next: users => this.setUsers(users),
348
error: error => this.setError(error.message)
349
}),
350
catchError(() => of([]))
351
)
352
)
353
)
354
);
355
356
// Optimistic updates
357
readonly updateUserOptimistic = this.effect<User>(user$ =>
358
user$.pipe(
359
tap(user => {
360
// Optimistically update the UI
361
this.updateUser(user);
362
}),
363
switchMap(user =>
364
this.userService.updateUser(user).pipe(
365
tap({
366
next: updatedUser => {
367
// Confirm the update with server response
368
this.updateUser(updatedUser);
369
},
370
error: error => {
371
// Revert optimistic update on error
372
this.loadUsers();
373
this.setError(error.message);
374
}
375
}),
376
catchError(() => of(null))
377
)
378
)
379
)
380
);
381
382
// Pagination support
383
readonly loadPage = this.effect<{ page: number; pageSize: number }>(
384
pageInfo$ => pageInfo$.pipe(
385
tap(() => this.setLoading(true)),
386
switchMap(({ page, pageSize }) =>
387
this.userService.getUsersPage(page, pageSize).pipe(
388
tap({
389
next: result => {
390
this.patchState({
391
users: result.data,
392
currentPage: page,
393
totalPages: result.totalPages,
394
loading: false
395
});
396
},
397
error: error => this.setError(error.message)
398
}),
399
catchError(() => of(null))
400
)
401
)
402
)
403
);
404
}
405
```
406
407
### Component Store Testing
408
409
Generated component stores include comprehensive testing setup:
410
411
```typescript
412
// Component store testing
413
describe('UserStore', () => {
414
let store: UserStore;
415
let userService: jasmine.SpyObj<UserService>;
416
417
beforeEach(() => {
418
const spy = jasmine.createSpyObj('UserService', [
419
'getUsers',
420
'createUser',
421
'updateUser',
422
'deleteUser'
423
]);
424
425
TestBed.configureTestingModule({
426
providers: [
427
UserStore,
428
{ provide: UserService, useValue: spy }
429
]
430
});
431
432
store = TestBed.inject(UserStore);
433
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
434
});
435
436
it('should initialize with default state', (done) => {
437
store.state$.subscribe(state => {
438
expect(state.users).toEqual([]);
439
expect(state.loading).toBeFalse();
440
expect(state.error).toBeNull();
441
done();
442
});
443
});
444
445
it('should load users', (done) => {
446
const users = [{ id: '1', name: 'John', email: 'john@example.com' }];
447
userService.getUsers.and.returnValue(of(users));
448
449
store.users$.subscribe(result => {
450
if (result.length > 0) {
451
expect(result).toEqual(users);
452
done();
453
}
454
});
455
456
store.loadUsers();
457
});
458
459
it('should add user', (done) => {
460
const newUser = { id: '1', name: 'John', email: 'john@example.com' };
461
462
store.addUser(newUser);
463
464
store.users$.subscribe(users => {
465
if (users.length > 0) {
466
expect(users).toContain(newUser);
467
done();
468
}
469
});
470
});
471
472
it('should update user', (done) => {
473
const user = { id: '1', name: 'John', email: 'john@example.com' };
474
const updatedUser = { ...user, name: 'John Updated' };
475
476
store.addUser(user);
477
store.updateUser(updatedUser);
478
479
store.users$.subscribe(users => {
480
const foundUser = users.find(u => u.id === '1');
481
if (foundUser && foundUser.name === 'John Updated') {
482
expect(foundUser.name).toBe('John Updated');
483
done();
484
}
485
});
486
});
487
488
it('should handle errors', (done) => {
489
const errorMessage = 'Network error';
490
userService.getUsers.and.returnValue(throwError({ message: errorMessage }));
491
492
store.error$.subscribe(error => {
493
if (error) {
494
expect(error).toBe(errorMessage);
495
done();
496
}
497
});
498
499
store.loadUsers();
500
});
501
});
502
```
503
504
### Component Store vs Global Store
505
506
Comparison of when to use component store vs global NgRx store:
507
508
```typescript { .api }
509
/**
510
* Component Store use cases
511
*/
512
interface ComponentStoreUseCases {
513
/** Local component state */
514
localState: 'State specific to single component/feature';
515
/** Temporary data */
516
temporaryData: 'Data that doesn\'t need to persist across routes';
517
/** Form state */
518
formState: 'Complex form state management';
519
/** UI state */
520
uiState: 'Modal state, filters, pagination';
521
/** Isolated features */
522
isolatedFeatures: 'Self-contained features with own lifecycle';
523
}
524
525
/**
526
* Global Store use cases
527
*/
528
interface GlobalStoreUseCases {
529
/** Shared state */
530
sharedState: 'State shared across multiple components';
531
/** Persistent data */
532
persistentData: 'Data that needs to survive route changes';
533
/** User authentication */
534
authentication: 'User session and auth state';
535
/** Application configuration */
536
configuration: 'Global app settings and config';
537
/** Complex workflows */
538
complexWorkflows: 'Multi-step processes across components';
539
}
540
```
541
542
### Performance Benefits
543
544
Component stores provide several performance advantages:
545
546
```typescript { .api }
547
/**
548
* Component store performance benefits
549
*/
550
interface PerformanceBenefits {
551
/** Scoped state */
552
scopedState: 'State lifecycle tied to component lifecycle';
553
/** Automatic cleanup */
554
automaticCleanup: 'State cleaned up when component destroyed';
555
/** Reduced global state */
556
reducedGlobalState: 'Less pollution of global state tree';
557
/** Reactive updates */
558
reactiveUpdates: 'Efficient reactive state updates';
559
/** OnPush compatibility */
560
onPushCompatible: 'Works seamlessly with OnPush change detection';
561
}
562
```