0
# Feature Management
1
2
Feature management enables modular state organization with lazy-loaded features, automatic selector generation, and feature state isolation. It provides a structured approach to organizing large applications with multiple domains.
3
4
## Capabilities
5
6
### Create Feature
7
8
Creates feature objects with automatic selector generation and optional extra selectors for modular state management.
9
10
```typescript { .api }
11
/**
12
* Creates a feature object with automatic selector generation
13
* @param config - Feature configuration with name and reducer
14
* @returns Feature object with selectors for state access
15
*/
16
function createFeature<FeatureName extends string, FeatureState>(
17
config: FeatureConfig<FeatureName, FeatureState>
18
): Feature<FeatureName, FeatureState>;
19
20
/**
21
* Creates a feature object with extra custom selectors
22
* @param config - Feature configuration with name, reducer, and extra selectors factory
23
* @returns Feature object with base and extra selectors
24
*/
25
function createFeature<FeatureName extends string, FeatureState, ExtraSelectors extends SelectorsDictionary>(
26
featureConfig: FeatureConfig<FeatureName, FeatureState> & {
27
extraSelectors: ExtraSelectorsFactory<FeatureName, FeatureState, ExtraSelectors>;
28
}
29
): FeatureWithExtraSelectors<FeatureName, FeatureState, ExtraSelectors>;
30
31
/**
32
* Basic feature configuration
33
*/
34
interface FeatureConfig<FeatureName extends string, FeatureState> {
35
/** Unique name/key for the feature */
36
name: FeatureName;
37
/** Reducer function for the feature state */
38
reducer: ActionReducer<FeatureState>;
39
}
40
```
41
42
**Usage Examples:**
43
44
```typescript
45
import { createFeature, createReducer, on } from "@ngrx/store";
46
import { loadUsers, loadUsersSuccess, loadUsersFailure } from "./user.actions";
47
48
// Define feature state
49
interface UserState {
50
entities: User[];
51
selectedUserId: string | null;
52
loading: boolean;
53
error: string | null;
54
filters: {
55
searchTerm: string;
56
role: string;
57
active: boolean;
58
};
59
}
60
61
const initialState: UserState = {
62
entities: [],
63
selectedUserId: null,
64
loading: false,
65
error: null,
66
filters: {
67
searchTerm: '',
68
role: 'all',
69
active: true
70
}
71
};
72
73
// Create feature reducer
74
const userReducer = createReducer(
75
initialState,
76
on(loadUsers, (state) => ({ ...state, loading: true, error: null })),
77
on(loadUsersSuccess, (state, { users }) => ({
78
...state,
79
entities: users,
80
loading: false
81
})),
82
on(loadUsersFailure, (state, { error }) => ({
83
...state,
84
loading: false,
85
error
86
}))
87
);
88
89
// Basic feature creation
90
export const userFeature = createFeature({
91
name: 'users',
92
reducer: userReducer
93
});
94
95
// Generated selectors automatically available:
96
// userFeature.selectUsersState - selects entire feature state
97
// userFeature.selectEntities - selects entities property
98
// userFeature.selectSelectedUserId - selects selectedUserId property
99
// userFeature.selectLoading - selects loading property
100
// userFeature.selectError - selects error property
101
// userFeature.selectFilters - selects filters property
102
103
// Usage in components
104
@Component({
105
template: `
106
<div>Users: {{ users() | json }}</div>
107
<div>Loading: {{ loading() }}</div>
108
`
109
})
110
export class UserListComponent {
111
private store = inject(Store);
112
113
users = this.store.selectSignal(userFeature.selectEntities);
114
loading = this.store.selectSignal(userFeature.selectLoading);
115
selectedUser = this.store.selectSignal(userFeature.selectSelectedUserId);
116
}
117
```
118
119
### Feature with Extra Selectors
120
121
Creates features with custom selectors in addition to automatically generated ones.
122
123
```typescript { .api }
124
/**
125
* Factory function for creating extra selectors
126
*/
127
type ExtraSelectorsFactory<FeatureName extends string, FeatureState, ExtraSelectors> =
128
(baseSelectors: BaseSelectors<FeatureName, FeatureState>) => ExtraSelectors;
129
130
/**
131
* Dictionary of selector functions
132
*/
133
type SelectorsDictionary = Record<
134
string,
135
| Selector<Record<string, any>, unknown>
136
| ((...args: any[]) => Selector<Record<string, any>, unknown>)
137
>;
138
```
139
140
**Usage Examples:**
141
142
```typescript
143
import { createFeature, createSelector } from "@ngrx/store";
144
145
// Feature with extra selectors
146
export const userFeature = createFeature({
147
name: 'users',
148
reducer: userReducer,
149
extraSelectors: ({ selectUsersState, selectEntities, selectFilters, selectSelectedUserId }) => ({
150
// Derived selectors using base selectors
151
selectActiveUsers: createSelector(
152
selectEntities,
153
(entities) => entities.filter(user => user.active)
154
),
155
156
selectFilteredUsers: createSelector(
157
selectEntities,
158
selectFilters,
159
(entities, filters) => entities.filter(user => {
160
const matchesSearch = user.name.toLowerCase().includes(filters.searchTerm.toLowerCase());
161
const matchesRole = filters.role === 'all' || user.role === filters.role;
162
const matchesActive = user.active === filters.active;
163
return matchesSearch && matchesRole && matchesActive;
164
})
165
),
166
167
selectSelectedUser: createSelector(
168
selectEntities,
169
selectSelectedUserId,
170
(entities, selectedId) => selectedId ? entities.find(u => u.id === selectedId) : null
171
),
172
173
selectUserCount: createSelector(
174
selectEntities,
175
(entities) => entities.length
176
),
177
178
selectUsersByRole: createSelector(
179
selectEntities,
180
(entities) => entities.reduce((acc, user) => {
181
if (!acc[user.role]) acc[user.role] = [];
182
acc[user.role].push(user);
183
return acc;
184
}, {} as Record<string, User[]>)
185
),
186
187
// Selector factory for parameterized selection
188
selectUserById: (id: string) => createSelector(
189
selectEntities,
190
(entities) => entities.find(user => user.id === id)
191
)
192
})
193
});
194
195
// All selectors now available:
196
// Base: selectUsersState, selectEntities, selectLoading, etc.
197
// Extra: selectActiveUsers, selectFilteredUsers, selectSelectedUser, etc.
198
199
// Usage with extra selectors
200
@Component({
201
template: `
202
<div>Active Users: {{ activeUsers() | json }}</div>
203
<div>Filtered Users: {{ filteredUsers() | json }}</div>
204
<div>Selected: {{ selectedUser()?.name }}</div>
205
`
206
})
207
export class UserDashboardComponent {
208
private store = inject(Store);
209
210
activeUsers = this.store.selectSignal(userFeature.selectActiveUsers);
211
filteredUsers = this.store.selectSignal(userFeature.selectFilteredUsers);
212
selectedUser = this.store.selectSignal(userFeature.selectSelectedUser);
213
usersByRole = this.store.selectSignal(userFeature.selectUsersByRole);
214
}
215
```
216
217
## Automatic Selector Generation
218
219
Features automatically generate selectors based on the state structure:
220
221
### Feature State Selector
222
223
```typescript
224
// For feature named 'users', generates:
225
selectUsersState: MemoizedSelector<Record<string, any>, UserState>
226
```
227
228
### Property Selectors
229
230
```typescript
231
// For each property in the state, generates selectors like:
232
selectEntities: MemoizedSelector<Record<string, any>, User[]>
233
selectLoading: MemoizedSelector<Record<string, any>, boolean>
234
selectError: MemoizedSelector<Record<string, any>, string | null>
235
```
236
237
### Nested Object Properties
238
239
```typescript
240
interface UserState {
241
filters: {
242
searchTerm: string;
243
role: string;
244
active: boolean;
245
};
246
}
247
248
// Generates flat selectors for nested properties:
249
selectFilters: MemoizedSelector<Record<string, any>, UserFilters>
250
selectSearchTerm: MemoizedSelector<Record<string, any>, string>
251
selectRole: MemoizedSelector<Record<string, any>, string>
252
selectActive: MemoizedSelector<Record<string, any>, boolean>
253
```
254
255
## Integration with Providers
256
257
Features work seamlessly with both module and standalone configurations:
258
259
### Module Configuration
260
261
```typescript
262
import { StoreModule } from "@ngrx/store";
263
import { userFeature } from "./user.feature";
264
265
@NgModule({
266
imports: [
267
StoreModule.forFeature(userFeature)
268
]
269
})
270
export class UserModule {}
271
```
272
273
### Standalone Configuration
274
275
```typescript
276
import { provideState } from "@ngrx/store";
277
import { userFeature } from "./user.feature";
278
279
export const appConfig: ApplicationConfig = {
280
providers: [
281
provideState(userFeature),
282
// other providers
283
]
284
};
285
```
286
287
## Advanced Feature Patterns
288
289
### Multi-Entity Features
290
291
```typescript
292
interface ProductState {
293
products: {
294
entities: Product[];
295
selectedId: string | null;
296
loading: boolean;
297
};
298
categories: {
299
entities: Category[];
300
selectedId: string | null;
301
loading: boolean;
302
};
303
reviews: {
304
entities: Review[];
305
productReviews: Record<string, string[]>;
306
loading: boolean;
307
};
308
}
309
310
export const productFeature = createFeature({
311
name: 'products',
312
reducer: productReducer,
313
extraSelectors: ({
314
selectProducts,
315
selectCategories,
316
selectReviews
317
}) => ({
318
selectProductsWithCategories: createSelector(
319
selectProducts,
320
selectCategories,
321
(products, categories) => products.entities.map(product => ({
322
...product,
323
category: categories.entities.find(c => c.id === product.categoryId)
324
}))
325
),
326
327
selectProductReviews: (productId: string) => createSelector(
328
selectReviews,
329
(reviews) => {
330
const reviewIds = reviews.productReviews[productId] || [];
331
return reviewIds.map(id => reviews.entities.find(r => r.id === id)).filter(Boolean);
332
}
333
),
334
335
selectFeaturedProducts: createSelector(
336
selectProducts,
337
selectCategories,
338
(products, categories) => products.entities
339
.filter(p => p.featured)
340
.map(product => ({
341
...product,
342
category: categories.entities.find(c => c.id === product.categoryId)
343
}))
344
)
345
})
346
});
347
```
348
349
### Feature Composition
350
351
```typescript
352
// Combine multiple features for complex selectors
353
export const dashboardSelectors = {
354
selectDashboardData: createSelector(
355
userFeature.selectActiveUsers,
356
productFeature.selectFeaturedProducts,
357
orderFeature.selectRecentOrders,
358
(users, products, orders) => ({
359
activeUserCount: users.length,
360
featuredProductCount: products.length,
361
recentOrderCount: orders.length,
362
revenue: orders.reduce((sum, order) => sum + order.total, 0)
363
})
364
)
365
};
366
```
367
368
## Type Safety and Constraints
369
370
Features include compile-time validation to ensure proper usage:
371
372
```typescript
373
// ❌ Optional properties not allowed in feature state
374
interface BadFeatureState {
375
required: string;
376
optional?: string; // Error: optional properties not allowed
377
}
378
379
// ✅ All properties must be required
380
interface GoodFeatureState {
381
required: string;
382
alsoRequired: string;
383
}
384
```
385
386
## Best Practices
387
388
1. **Feature Boundaries**: Organize features around business domains, not technical concerns
389
2. **State Shape**: Keep feature state flat and normalized when possible
390
3. **Selector Naming**: Use descriptive names for extra selectors that indicate their purpose
391
4. **Composition**: Combine features for cross-cutting concerns in separate selector files
392
5. **Testing**: Test features and their selectors independently with mock state
393
6. **Lazy Loading**: Use features with lazy-loaded modules for code splitting