Skip to content
Back to skills

Frontend Angular Store

ASecurity

Use when implementing state management with PlatformVmStore for complex components requiring reactive state, effects, and selectors.

  • 666 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 8, 2026
developmenttypescriptgobashreactangularapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill frontend-angular-store --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Frontend Angular Store?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Frontend Angular Store
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-frontend-angular-store/badge)](https://www.skillsdirectory.com/skills/majiayu000-frontend-angular-store)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-store
description: Use when implementing state management with PlatformVmStore for complex components requiring reactive state, effects, and selectors.
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
---

# Angular Store Development Workflow

## When to Use This Skill

- List components with CRUD operations
- Complex state with multiple data sources
- Shared state between components
- Caching and reloading patterns

## Pre-Flight Checklist

- [ ] Identify state shape (what data is needed)
- [ ] **Read the design system docs** for the target application (see below)
- [ ] Identify side effects (API calls, etc.)
- [ ] Search similar stores: `grep "{Feature}Store" --include="*.ts"`
- [ ] Determine caching requirements

## 🎨 Design System Documentation (MANDATORY)

**Before creating any store, read the design system documentation for your target application:**

| Application                       | Design System Location                           |
| --------------------------------- | ------------------------------------------------ |
| **WebV2 Apps**                    | `docs/design-system/`                            |
| **TextSnippetClient**             | `src/PlatformExampleAppWeb/apps/playground-text-snippet/docs/design-system/` |

**Key docs to read:**

- `README.md` - Component overview, base classes, library summary
- `06-state-management.md` - State management patterns (NgRx, PlatformVmStore)
- `07-technical-guide.md` - Implementation checklist, best practices

## File Location

```
src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
    └── {feature}/
        β”œβ”€β”€ {feature}.store.ts
        └── {feature}.component.ts
```

## Store Architecture

```
PlatformVmStore<TState>
β”œβ”€β”€ State: TState (reactive signal)
β”œβ”€β”€ Selectors: select() β†’ Signal<T>
β”œβ”€β”€ Effects: effectSimple() β†’ side effects
β”œβ”€β”€ Updaters: updateState() β†’ mutations
└── Loading/Error: observerLoadingErrorState()
```

## Pattern 1: Basic CRUD Store

```typescript
// {feature}-list.store.ts
import { Injectable } from '@angular/core';
import { PlatformVmStore } from '@libs/platform-core';

// ═══════════════════════════════════════════════════════════════════════════
// STATE INTERFACE
// ═══════════════════════════════════════════════════════════════════════════

export interface FeatureListState {
    items: FeatureDto[];
    selectedItem?: FeatureDto;
    filters: FeatureFilters;
    pagination: PaginationState;
}

export interface FeatureFilters {
    searchText?: string;
    status?: FeatureStatus[];
    dateRange?: DateRange;
}

export interface PaginationState {
    pageIndex: number;
    pageSize: number;
    totalCount: number;
}

// ═══════════════════════════════════════════════════════════════════════════
// STORE IMPLEMENTATION
// ═══════════════════════════════════════════════════════════════════════════

@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
    // ─────────────────────────────────────────────────────────────────────────
    // CONFIGURATION
    // ─────────────────────────────────────────────────────────────────────────

    // State factory
    protected override vmConstructor = (data?: Partial<FeatureListState>) =>
        ({
            items: [],
            filters: {},
            pagination: { pageIndex: 0, pageSize: 20, totalCount: 0 },
            ...data
        }) as FeatureListState;

    // Optional: Enable caching
    protected override get enableCaching() {
        return true;
    }
    protected override cachedStateKeyName = () => 'FeatureListStore';

    // ─────────────────────────────────────────────────────────────────────────
    // SELECTORS (Reactive Signals)
    // ─────────────────────────────────────────────────────────────────────────

    public readonly items$ = this.select(state => state.items);
    public readonly selectedItem$ = this.select(state => state.selectedItem);
    public readonly filters$ = this.select(state => state.filters);
    public readonly pagination$ = this.select(state => state.pagination);

    // Derived selectors
    public readonly hasItems$ = this.select(state => state.items.length > 0);
    public readonly isEmpty$ = this.select(state => state.items.length === 0);

    // ─────────────────────────────────────────────────────────────────────────
    // EFFECTS (Side Effects)
    // ─────────────────────────────────────────────────────────────────────────

    // Load items with current filters
    public loadItems = this.effectSimple(() => {
        const state = this.currentVm();
        return this.featureApi
            .getList({
                ...state.filters,
                skipCount: state.pagination.pageIndex * state.pagination.pageSize,
                maxResultCount: state.pagination.pageSize
            })
            .pipe(
                this.tapResponse(result => {
                    this.updateState({
                        items: result.items,
                        pagination: {
                            ...state.pagination,
                            totalCount: result.totalCount
                        }
                    });
                })
            );
    }, 'loadItems');

    // Save item (create or update)
    public saveItem = this.effectSimple(
        (item: FeatureDto) =>
            this.featureApi.save(item).pipe(
                this.tapResponse(saved => {
                    this.updateState(state => ({
                        items: state.items.upsertBy(x => x.id, [saved]),
                        selectedItem: saved
                    }));
                })
            ),
        'saveItem'
    );

    // Delete item
    public deleteItem = this.effectSimple(
        (id: string) =>
            this.featureApi.delete(id).pipe(
                this.tapResponse(() => {
                    this.updateState(state => ({
                        items: state.items.filter(x => x.id !== id),
                        selectedItem: state.selectedItem?.id === id ? undefined : state.selectedItem
                    }));
                })
            ),
        'deleteItem'
    );

    // ─────────────────────────────────────────────────────────────────────────
    // STATE UPDATERS
    // ─────────────────────────────────────────────────────────────────────────

    public setFilters(filters: Partial<FeatureFilters>): void {
        this.updateState(state => ({
            filters: { ...state.filters, ...filters },
            pagination: { ...state.pagination, pageIndex: 0 } // Reset to first page
        }));
    }

    public setPage(pageIndex: number): void {
        this.updateState(state => ({
            pagination: { ...state.pagination, pageIndex }
        }));
    }

    public selectItem(item?: FeatureDto): void {
        this.updateState({ selectedItem: item });
    }

    public clearFilters(): void {
        this.updateState({
            filters: {},
            pagination: { ...this.currentVm().pagination, pageIndex: 0 }
        });
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CONSTRUCTOR
    // ─────────────────────────────────────────────────────────────────────────

    constructor(private featureApi: FeatureApiService) {
        super();
    }
}
```

## Pattern 2: Store with Dependent Data

```typescript
@Injectable()
export class EmployeeFormStore extends PlatformVmStore<EmployeeFormState> {
    protected override vmConstructor = (data?: Partial<EmployeeFormState>) =>
        ({
            employee: null,
            departments: [],
            positions: [],
            managers: [],
            ...data
        }) as EmployeeFormState;

    // Load all dependent data in parallel
    public loadFormData = this.effectSimple(
        (employeeId?: string) =>
            forkJoin({
                employee: employeeId ? this.employeeApi.getById(employeeId) : of(this.createNewEmployee()),
                departments: this.departmentApi.getActive(),
                positions: this.positionApi.getAll(),
                managers: this.employeeApi.getManagers()
            }).pipe(
                this.tapResponse(result => {
                    this.updateState({
                        employee: result.employee,
                        departments: result.departments,
                        positions: result.positions,
                        managers: result.managers
                    });
                })
            ),
        'loadFormData'
    );

    // Dependent selector - filter managers by department
    public managersForDepartment$ = (departmentId: string) => this.select(state => state.managers.filter(m => m.departmentId === departmentId));
}
```

## Pattern 3: Store with Caching

```typescript
@Injectable({ providedIn: 'root' }) // Singleton for caching
export class LookupDataStore extends PlatformVmStore<LookupDataState> {
    protected override get enableCaching() {
        return true;
    }
    protected override cachedStateKeyName = () => 'LookupDataStore';

    // Cache timeout (optional)
    protected override get cacheExpirationMs() {
        return 5 * 60 * 1000;
    } // 5 minutes

    // Load with cache check
    public loadCountries = this.effectSimple(() => {
        if (this.currentVm().countries.length > 0) {
            return EMPTY; // Already loaded, skip
        }

        return this.lookupApi.getCountries().pipe(
            this.tapResponse(countries => {
                this.updateState({ countries });
            })
        );
    }, 'loadCountries');
}
```

## Component Integration

```typescript
@Component({
    selector: 'app-feature-list',
    templateUrl: './feature-list.component.html',
    providers: [FeatureListStore] // Component-scoped store
})
export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit {
    constructor(store: FeatureListStore) {
        super(store);
    }

    ngOnInit(): void {
        this.store.loadItems();
    }

    onSearch(text: string): void {
        this.store.setFilters({ searchText: text });
        this.store.loadItems();
    }

    onPageChange(pageIndex: number): void {
        this.store.setPage(pageIndex);
        this.store.loadItems();
    }

    onDelete(item: FeatureDto): void {
        this.store.deleteItem(item.id);
    }

    onRefresh(): void {
        this.reload(); // Inherited - reloads all store effects
    }

    // Loading states
    get isLoading$() {
        return this.store.isLoading$('loadItems');
    }
    get isSaving$() {
        return this.store.isLoading$('saveItem');
    }
    get isDeleting$() {
        return this.store.isLoading$('deleteItem');
    }
}
```

## Template Usage

```html
<app-loading-and-error-indicator [target]="this">
    @if (vm(); as vm) {
    <!-- Filters -->
    <div class="filters">
        <input [value]="vm.filters.searchText ?? ''" (input)="onSearch($event.target.value)" placeholder="Search..." />
    </div>

    <!-- List -->
    @for (item of vm.items; track item.id) {
    <div class="item" [class.selected]="vm.selectedItem?.id === item.id">
        {{ item.name }}
        <button (click)="onDelete(item)" [disabled]="isDeleting$()">Delete</button>
    </div>
    } @empty {
    <div class="empty">No items found</div>
    }

    <!-- Pagination -->
    <app-pagination
        [pageIndex]="vm.pagination.pageIndex"
        [pageSize]="vm.pagination.pageSize"
        [totalCount]="vm.pagination.totalCount"
        (pageChange)="onPageChange($event)"
    />
    }
</app-loading-and-error-indicator>
```

## Key Store APIs

| Method                        | Purpose              | Example                                                   |
| ----------------------------- | -------------------- | --------------------------------------------------------- |
| `select()`                    | Create selector      | `this.select(s => s.items)`                               |
| `updateState()`               | Update state         | `this.updateState({ items })`                             |
| `effectSimple()`              | Create effect        | `this.effectSimple(() => api.call(), 'requestKey')`       |
| `currentVm()`                 | Get current state    | `const state = this.currentVm()`                          |
| `observerLoadingErrorState()` | Track loading/error  | Use outside effectSimple only (effectSimple handles this) |
| `tapResponse()`               | Handle success/error | `.pipe(this.tapResponse(success, error))`                 |
| `isLoading$()`                | Loading signal       | `this.store.isLoading$('loadItems')`                      |

## Anti-Patterns to AVOID

:x: **Calling effects without tracking**

```typescript
// WRONG - no loading state
this.api.getItems().subscribe(items => this.updateState({ items }));

// CORRECT - effectSimple auto-tracks loading state via second parameter
public loadItems = this.effectSimple(
    () => this.api.getItems().pipe(
        this.tapResponse(items => this.updateState({ items }))
    ),
    'loadItems'
);
```

:x: **Mutating state directly**

```typescript
// WRONG - direct mutation
this.currentVm().items.push(newItem);

// CORRECT - immutable update
this.updateState(state => ({
    items: [...state.items, newItem]
}));
```

:x: **Using store without provider**

```typescript
// WRONG - no provider
export class MyComponent {
  constructor(private store: FeatureStore) { }  // Error: No provider
}

// CORRECT - provide at component level
@Component({
  providers: [FeatureStore]
})
```

## Verification Checklist

- [ ] State interface defines all required properties
- [ ] `vmConstructor` provides default state
- [ ] Effects use `effectSimple()` with request key as second parameter
- [ ] Effects use `tapResponse()` for handling
- [ ] Selectors are memoized with `select()`
- [ ] State updates are immutable
- [ ] Store provided at correct level (component vs root)
- [ ] Caching configured if needed

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.