0
# Container Components
1
2
NgRx container component generation schematic that creates smart container components that connect to the NgRx store and manage state interactions. Container components serve as the bridge between the state management layer and presentation components.
3
4
## Capabilities
5
6
### Container Schematic
7
8
Generates Angular components that connect to NgRx store for state management and dispatch actions.
9
10
```bash
11
# Basic container component
12
ng generate @ngrx/schematics:container UserList
13
14
# Container with custom state interface
15
ng generate @ngrx/schematics:container ProductCatalog --stateInterface=CatalogState
16
17
# Container with specific state path
18
ng generate @ngrx/schematics:container OrderDashboard --state=order
19
```
20
21
```typescript { .api }
22
/**
23
* Container schematic configuration interface
24
*/
25
interface ContainerSchema {
26
/** Name of the container component */
27
name: string;
28
/** Path where container files should be generated */
29
path?: string;
30
/** Angular project to target */
31
project?: string;
32
/** Specifies if the style will be in the ts file */
33
inlineStyle?: boolean;
34
/** Specifies if the template will be in the ts file */
35
inlineTemplate?: boolean;
36
/** Specifies the view encapsulation strategy */
37
viewEncapsulation?: 'Emulated' | 'Native' | 'None';
38
/** Specifies the change detection strategy */
39
changeDetection?: 'Default' | 'OnPush';
40
/** The prefix to apply to generated selectors */
41
prefix?: string;
42
/** The file extension or preprocessor to use for style files */
43
style?: string;
44
/** When true, does not create test files */
45
skipTests?: boolean;
46
/** Generate files without creating a folder */
47
flat?: boolean;
48
/** Flag to skip the module import */
49
skipImport?: boolean;
50
/** The selector to use for the component */
51
selector?: string;
52
/** Allows specification of the declaring module */
53
module?: string;
54
/** Specifies if declaring module exports the component */
55
export?: boolean;
56
/** State slice to connect to */
57
state?: string;
58
/** Name of the state interface */
59
stateInterface?: string;
60
/** Specifies whether to create a unit test or an integration test */
61
testDepth?: 'unit' | 'integration';
62
/** Whether the generated component is standalone */
63
standalone?: boolean;
64
/** Specifies if the style will contain :host { display: block; } */
65
displayBlock?: boolean;
66
}
67
```
68
69
### Generated Container Component
70
71
Creates a complete Angular component with NgRx store integration:
72
73
```typescript
74
// Generated container component
75
import { Component, OnInit } from '@angular/core';
76
import { Store } from '@ngrx/store';
77
import { Observable } from 'rxjs';
78
import * as UserActions from '../state/user.actions';
79
import * as fromUser from '../state';
80
import { User } from '../models/user.model';
81
82
@Component({
83
selector: 'app-user-list',
84
templateUrl: './user-list.component.html',
85
styleUrls: ['./user-list.component.scss']
86
})
87
export class UserListComponent implements OnInit {
88
users$: Observable<User[]>;
89
loading$: Observable<boolean>;
90
error$: Observable<string | null>;
91
selectedUser$: Observable<User | null>;
92
93
constructor(private store: Store) {
94
this.users$ = this.store.select(fromUser.selectAllUsers);
95
this.loading$ = this.store.select(fromUser.selectUsersLoading);
96
this.error$ = this.store.select(fromUser.selectUsersError);
97
this.selectedUser$ = this.store.select(fromUser.selectSelectedUser);
98
}
99
100
ngOnInit(): void {
101
this.loadUsers();
102
}
103
104
loadUsers(): void {
105
this.store.dispatch(UserActions.loadUsers());
106
}
107
108
selectUser(user: User): void {
109
this.store.dispatch(UserActions.selectUser({ userId: user.id }));
110
}
111
112
createUser(user: User): void {
113
this.store.dispatch(UserActions.createUser({ user }));
114
}
115
116
updateUser(user: User): void {
117
this.store.dispatch(UserActions.updateUser({ user }));
118
}
119
120
deleteUser(userId: string): void {
121
this.store.dispatch(UserActions.deleteUser({ id: userId }));
122
}
123
124
clearSelection(): void {
125
this.store.dispatch(UserActions.clearSelection());
126
}
127
}
128
```
129
130
**Usage Examples:**
131
132
```bash
133
# Generate user list container
134
ng generate @ngrx/schematics:container UserList --state=user
135
136
# Generate product catalog container
137
ng generate @ngrx/schematics:container ProductCatalog --path=src/app/catalog --state=product
138
139
# Generate order dashboard with custom state interface
140
ng generate @ngrx/schematics:container OrderDashboard --stateInterface=OrderState
141
```
142
143
### Generated Template
144
145
Creates a complete template with state binding and event handling:
146
147
```html
148
<!-- Generated container template -->
149
<div class="user-list-container">
150
<h2>Users</h2>
151
152
<!-- Loading indicator -->
153
<div *ngIf="loading$ | async" class="loading">
154
Loading users...
155
</div>
156
157
<!-- Error display -->
158
<div *ngIf="error$ | async as error" class="error">
159
Error: {{ error }}
160
<button (click)="loadUsers()">Retry</button>
161
</div>
162
163
<!-- User list -->
164
<div *ngIf="!(loading$ | async) && !(error$ | async)">
165
<div class="user-actions">
166
<button (click)="loadUsers()">Refresh</button>
167
<button (click)="clearSelection()">Clear Selection</button>
168
</div>
169
170
<div class="user-grid">
171
<div
172
*ngFor="let user of users$ | async; trackBy: trackByUserId"
173
class="user-card"
174
[class.selected]="(selectedUser$ | async)?.id === user.id"
175
(click)="selectUser(user)">
176
177
<h3>{{ user.name }}</h3>
178
<p>{{ user.email }}</p>
179
<p>Status: {{ user.active ? 'Active' : 'Inactive' }}</p>
180
181
<div class="user-actions">
182
<button (click)="updateUser(user); $event.stopPropagation()">
183
Edit
184
</button>
185
<button (click)="deleteUser(user.id); $event.stopPropagation()">
186
Delete
187
</button>
188
</div>
189
</div>
190
</div>
191
192
<!-- No users message -->
193
<div *ngIf="(users$ | async)?.length === 0" class="no-users">
194
No users found.
195
</div>
196
</div>
197
198
<!-- Selected user details -->
199
<div *ngIf="selectedUser$ | async as selectedUser" class="selected-user">
200
<h3>Selected User</h3>
201
<pre>{{ selectedUser | json }}</pre>
202
</div>
203
</div>
204
```
205
206
### Generated Styles
207
208
Creates basic styles for the container component:
209
210
```scss
211
// Generated container styles
212
.user-list-container {
213
padding: 20px;
214
215
.loading, .error {
216
padding: 10px;
217
margin: 10px 0;
218
border-radius: 4px;
219
}
220
221
.loading {
222
background-color: #e3f2fd;
223
color: #1976d2;
224
}
225
226
.error {
227
background-color: #ffebee;
228
color: #c62828;
229
230
button {
231
margin-left: 10px;
232
padding: 5px 10px;
233
background-color: #c62828;
234
color: white;
235
border: none;
236
border-radius: 4px;
237
cursor: pointer;
238
}
239
}
240
241
.user-actions {
242
margin: 10px 0;
243
244
button {
245
margin-right: 10px;
246
padding: 8px 16px;
247
background-color: #1976d2;
248
color: white;
249
border: none;
250
border-radius: 4px;
251
cursor: pointer;
252
253
&:hover {
254
background-color: #1565c0;
255
}
256
}
257
}
258
259
.user-grid {
260
display: grid;
261
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
262
gap: 16px;
263
margin: 20px 0;
264
}
265
266
.user-card {
267
border: 1px solid #ddd;
268
border-radius: 8px;
269
padding: 16px;
270
cursor: pointer;
271
transition: all 0.2s ease;
272
273
&:hover {
274
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
275
}
276
277
&.selected {
278
border-color: #1976d2;
279
background-color: #e3f2fd;
280
}
281
282
h3 {
283
margin: 0 0 8px 0;
284
color: #333;
285
}
286
287
p {
288
margin: 4px 0;
289
color: #666;
290
}
291
292
.user-actions {
293
margin-top: 12px;
294
295
button {
296
margin-right: 8px;
297
padding: 4px 8px;
298
font-size: 12px;
299
}
300
}
301
}
302
303
.no-users {
304
text-align: center;
305
padding: 40px;
306
color: #666;
307
font-style: italic;
308
}
309
310
.selected-user {
311
margin-top: 20px;
312
padding: 16px;
313
background-color: #f5f5f5;
314
border-radius: 8px;
315
316
h3 {
317
margin-top: 0;
318
}
319
320
pre {
321
background-color: white;
322
padding: 10px;
323
border-radius: 4px;
324
overflow-x: auto;
325
}
326
}
327
}
328
```
329
330
### Smart vs Presentational Pattern
331
332
The container follows the smart/presentational component pattern:
333
334
```typescript { .api }
335
/**
336
* Container component responsibilities (Smart Component)
337
*/
338
interface SmartComponentPattern {
339
/** State management */
340
stateSubscription: 'Subscribe to store state via selectors';
341
/** Action dispatching */
342
actionDispatching: 'Dispatch actions to modify state';
343
/** Data fetching */
344
dataFetching: 'Trigger data loading operations';
345
/** Business logic */
346
businessLogic: 'Handle complex business operations';
347
/** Route handling */
348
routeHandling: 'React to route changes';
349
}
350
351
/**
352
* Presentational component pattern (for reference)
353
*/
354
interface PresentationalComponentPattern {
355
/** Pure display */
356
pureDisplay: 'Display data received via @Input()';
357
/** Event emission */
358
eventEmission: 'Emit events via @Output()';
359
/** No dependencies */
360
noDependencies: 'No direct dependencies on services';
361
/** Reusable */
362
reusable: 'Can be reused in different contexts';
363
}
364
```
365
366
### Advanced Container Patterns
367
368
Generated containers can include advanced patterns:
369
370
```typescript
371
// Advanced container with view models
372
export class UserListComponent implements OnInit, OnDestroy {
373
// View model selector combining multiple state slices
374
viewModel$ = this.store.select(fromUser.selectUserListViewModel);
375
376
// Track subscriptions for cleanup
377
private destroy$ = new Subject<void>();
378
379
constructor(private store: Store) {}
380
381
ngOnInit(): void {
382
this.loadUsers();
383
384
// Subscribe to route params for filtering
385
this.route.params.pipe(
386
takeUntil(this.destroy$)
387
).subscribe(params => {
388
if (params['filter']) {
389
this.store.dispatch(UserActions.setFilter({ filter: params['filter'] }));
390
}
391
});
392
}
393
394
ngOnDestroy(): void {
395
this.destroy$.next();
396
this.destroy$.complete();
397
}
398
399
// Track by function for performance
400
trackByUserId(index: number, user: User): string {
401
return user.id;
402
}
403
404
// Bulk operations
405
selectMultiple(userIds: string[]): void {
406
this.store.dispatch(UserActions.selectMultiple({ userIds }));
407
}
408
409
deleteMultiple(userIds: string[]): void {
410
this.store.dispatch(UserActions.deleteMultiple({ userIds }));
411
}
412
}
413
```
414
415
### Container Testing
416
417
Generated containers include comprehensive testing setup:
418
419
```typescript
420
// Container component testing
421
describe('UserListComponent', () => {
422
let component: UserListComponent;
423
let fixture: ComponentFixture<UserListComponent>;
424
let store: MockStore;
425
let mockUserState: UserState;
426
427
beforeEach(() => {
428
mockUserState = {
429
users: [
430
{ id: '1', name: 'John', email: 'john@example.com', active: true },
431
{ id: '2', name: 'Jane', email: 'jane@example.com', active: false }
432
],
433
loading: false,
434
error: null,
435
selectedUserId: null
436
};
437
438
TestBed.configureTestingModule({
439
declarations: [UserListComponent],
440
imports: [CommonModule],
441
providers: [
442
provideMockStore({ initialState: { user: mockUserState } })
443
]
444
});
445
446
fixture = TestBed.createComponent(UserListComponent);
447
component = fixture.componentInstance;
448
store = TestBed.inject(MockStore);
449
});
450
451
it('should create', () => {
452
expect(component).toBeTruthy();
453
});
454
455
it('should load users on init', () => {
456
spyOn(store, 'dispatch');
457
component.ngOnInit();
458
expect(store.dispatch).toHaveBeenCalledWith(UserActions.loadUsers());
459
});
460
461
it('should select user', () => {
462
const user = mockUserState.users[0];
463
spyOn(store, 'dispatch');
464
465
component.selectUser(user);
466
467
expect(store.dispatch).toHaveBeenCalledWith(
468
UserActions.selectUser({ userId: user.id })
469
);
470
});
471
472
it('should display users', fakeAsync(() => {
473
fixture.detectChanges();
474
tick();
475
476
const userCards = fixture.debugElement.queryAll(By.css('.user-card'));
477
expect(userCards.length).toBe(2);
478
expect(userCards[0].nativeElement.textContent).toContain('John');
479
}));
480
481
it('should show loading state', fakeAsync(() => {
482
store.setState({ user: { ...mockUserState, loading: true } });
483
fixture.detectChanges();
484
tick();
485
486
const loadingElement = fixture.debugElement.query(By.css('.loading'));
487
expect(loadingElement).toBeTruthy();
488
expect(loadingElement.nativeElement.textContent).toContain('Loading');
489
}));
490
});
491
```
492
493
### Container Integration
494
495
Generated containers integrate with Angular routing and modules:
496
497
```typescript
498
// Module integration
499
@NgModule({
500
declarations: [UserListComponent],
501
imports: [
502
CommonModule,
503
RouterModule.forChild([
504
{ path: '', component: UserListComponent }
505
])
506
]
507
})
508
export class UserModule {}
509
510
// Route integration with resolver
511
export const UserListRoute: Route = {
512
path: 'users',
513
component: UserListComponent,
514
resolve: {
515
users: UserResolver
516
}
517
};
518
```
519
520
### OnPush Change Detection
521
522
Optimized containers use OnPush change detection strategy:
523
524
```typescript
525
// Optimized container with OnPush
526
@Component({
527
selector: 'app-user-list',
528
templateUrl: './user-list.component.html',
529
styleUrls: ['./user-list.component.scss'],
530
changeDetection: ChangeDetectionStrategy.OnPush
531
})
532
export class UserListComponent implements OnInit {
533
// All properties are observables for OnPush compatibility
534
users$ = this.store.select(fromUser.selectAllUsers);
535
loading$ = this.store.select(fromUser.selectUsersLoading);
536
error$ = this.store.select(fromUser.selectUsersError);
537
538
constructor(private store: Store) {}
539
540
// Methods remain the same - they dispatch actions
541
loadUsers(): void {
542
this.store.dispatch(UserActions.loadUsers());
543
}
544
}
545
```