or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

action-generation.mdcomponent-store.mdcontainer-components.mddata-services.mdeffect-generation.mdentity-management.mdfeature-generation.mdindex.mdngrx-push-migration.mdreducer-generation.mdselector-generation.mdstore-setup.mdutility-functions.md

component-store.mddocs/

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

```