or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

action-creators.mdfeature-management.mdindex.mdmodule-configuration.mdreducer-creators.mdselectors.mdstandalone-providers.mdstore-service.mdtesting-utilities.md

feature-management.mddocs/

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