本文へ移動
cccskills
無料GitHub で公開

angular-state-and-rx

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

インストール方法を見る

含まれるファイル(1)

  • SKILL.md16.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Angular State + RxJS Patterns

Modern Angular era (17+) introduces signals as a first-class reactivity primitive alongside RxJS. Both have a place — signals для component state, RxJS для async streams. Plus optional NgRx variants for complex apps.

Decision tree

NeedTool
Local component state (count, isOpen)signal()
Computed from other statecomputed()
Side effects on signal changeeffect() (sparingly)
Cross-component shared state@Injectable({ providedIn: 'root' }) service з signals
Server data with cachingTanStack Query Angular OR custom RxJS service з shareReplay
Complex domain state з time-travel debuggingNgRx Store + Effects + Selectors
Lightweight per-component reactive storeNgRx Component Store
Signal-based store API@ngrx/signals (Angular 17+)
URL stateActivatedRoute snapshot / queryParams

Detect what's installed; mirror project's choice; never introduce a new state lib without BA approval.

Signals (Angular 17+)

import { signal, computed, effect } from '@angular/core';

count = signal(0);
double = computed(() => this.count() * 2);

constructor() {
  effect(() => {
    console.log('count is now', this.count());
  });
}

increment() {
  this.count.update((c) => c + 1);          // functional update — best for derivations
}

set(value: number) {
  this.count.set(value);                     // direct set
}

reset() {
  this.count.set(0);
}

signal(initial) vs computed(fn) vs effect(fn)

  • signal() — writable, returns a function. Read with signal(), write with signal.set() or signal.update().
  • computed() — read-only, derived from other signals. Auto-tracks dependencies (calls inside the function are observed). Memoized — recomputes only when deps change.
  • effect() — runs after component init and re-runs when read signals change. Use SPARINGLY — most reactivity flows through templates (auto-track via {{ signal() }}).

When NOT to use effect()

  • ❌ For derived state — use computed().
  • ❌ For state synchronization — usually a sign of bad architecture.
  • ❌ For form-control updates — Angular Forms have their own reactivity.
  • ✅ For non-Angular DOM access (e.g., third-party charting library that needs imperative updates).
  • ✅ For logging / analytics on state changes.

Signal inputs/outputs (Angular 17.1+)

import { input, output, computed } from '@angular/core';

@Component({ ... })
export class UserCardComponent {
  user = input.required<User>();              // required input — InputSignal<User>
  showActions = input(false);                  // optional with default — InputSignal<boolean>
  delete = output<string>();                   // OutputEmitterRef<string>

  isAdmin = computed(() => this.user().role === 'admin');

  onDelete() {
    this.delete.emit(this.user().id);
  }
}

For new components in Angular 17.1+, prefer input() / output() over @Input() / @Output() decorators. Mirror existing project style if all components still use decorators.

Services as state (singleton pattern)

// users.service.ts
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { User } from './user.model';

@Injectable({ providedIn: 'root' })
export class UsersService {
  private http = inject(HttpClient);

  // Private writable signals
  private _users = signal<User[]>([]);
  private _loading = signal(false);
  private _error = signal<string | null>(null);

  // Public readonly signals
  users = this._users.asReadonly();
  loading = this._loading.asReadonly();
  error = this._error.asReadonly();

  // Public computed
  count = computed(() => this._users().length);
  hasError = computed(() => this._error() !== null);

  async loadUsers() {
    this._loading.set(true);
    this._error.set(null);
    try {
      const users = await firstValueFrom(this.http.get<User[]>('/api/users'));
      this._users.set(users);
    } catch (err) {
      this._error.set(err instanceof Error ? err.message : 'Failed to load');
    } finally {
      this._loading.set(false);
    }
  }

  addUser(user: User) {
    this._users.update((users) => [...users, user]);
  }

  removeUser(id: string) {
    this._users.update((users) => users.filter((u) => u.id !== id));
  }
}

asReadonly() exposes a read-only view — consumers can't .set() on it. Internal mutations stay encapsulated.

Component consumers:

@Component({
  template: `
    @if (usersService.loading()) {
      <p>Loading...</p>
    } @else {
      <p>{{ usersService.count() }} users</p>
      <ul>
        @for (user of usersService.users(); track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    }
  `
})
export class UsersListComponent {
  usersService = inject(UsersService);

  ngOnInit() {
    this.usersService.loadUsers();
  }
}

RxJS essentials

Angular still uses RxJS heavily — HttpClient returns Observables, ActivatedRoute params/queryParams are Observables, NgRx Effects are Observables.

Common operators

import { map, filter, switchMap, mergeMap, exhaustMap, debounceTime, distinctUntilChanged, tap, catchError, finalize, takeUntil, take, shareReplay } from 'rxjs';

this.search$.pipe(
  debounceTime(300),                     // wait 300ms after last input
  distinctUntilChanged(),                // skip duplicates
  switchMap((query) => this.searchService.search(query)),  // cancel prior on new input
  catchError((err) => of([])),           // error fallback
);
OperatorUse
map(fn)Transform values
filter(fn)Keep matching values
tap(fn)Side effect (logging) without changing stream
switchMap(fn)Cancel previous inner observable on new outer (search, autocomplete)
mergeMap(fn)Run all inner observables in parallel
concatMap(fn)Sequential — wait for prior to complete
exhaustMap(fn)Ignore new outer while inner pending (login, save buttons)
debounceTime(ms)Wait quiet period before emitting
distinctUntilChanged()Skip consecutive duplicates
take(n)Take first n then complete
takeUntil(notifier$)Stop when notifier emits (manual cleanup pattern)
shareReplay({ bufferSize: 1, refCount: true })Cache last value for late subscribers (HTTP caching pattern)
catchError(fn)Recover from errors
finalize(fn)Cleanup on complete or error

async pipe in templates

@if (users$ | async; as users) {
  @for (user of users; track user.id) {
    <p>{{ user.name }}</p>
  }
} @else {
  <p>Loading...</p>
}

async pipe handles subscribe + unsubscribe automatically. Always prefer over manual subscribe() in component classes.

takeUntilDestroyed() (Angular 16+)

For when you need subscribe() directly:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { inject, Component, OnInit } from '@angular/core';

@Component({ ... })
export class UserDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);

  ngOnInit() {
    this.route.params
      .pipe(takeUntilDestroyed())          // automatic cleanup tied to component
      .subscribe((params) => {
        // ...
      });
  }
}

takeUntilDestroyed() MUST be called in injection context — class field initializer or constructor. If called inside ngOnInit(), you must pass an explicit DestroyRef:

private destroyRef = inject(DestroyRef);
ngOnInit() {
  this.someStream$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...);
}

Replaces the older manual pattern:

// ❌ Pre-16 — verbose, easy to forget
private destroy$ = new Subject<void>();
ngOnInit() {
  this.something$.pipe(takeUntil(this.destroy$)).subscribe(...);
}
ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

firstValueFrom / lastValueFrom

For one-shot observables (HTTP), convert to Promise:

import { firstValueFrom } from 'rxjs';

async loadUser(id: string) {
  return await firstValueFrom(this.http.get<User>(`/api/users/${id}`));
}

Replaces deprecated .toPromise().

Bridging signals ↔ RxJS

import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Observable → Signal
@Injectable({ providedIn: 'root' })
export class UsersService {
  private http = inject(HttpClient);
  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
}

// Signal → Observable
const filter = signal('');
const filter$ = toObservable(filter);   // emits whenever filter() changes

filter$.pipe(
  debounceTime(300),
  switchMap((q) => this.search(q)),
).subscribe(...);

toSignal requires either initialValue OR requireSync: true (which throws if observable doesn't emit synchronously). Most HTTP cases need initialValue.

NgRx Store (when @ngrx/store detected)

pnpm add @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/entity

Setup (standalone)

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ users: usersReducer }),
    provideEffects([UsersEffects]),
    provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
  ],
});

Actions

// users.actions.ts
import { createAction, createActionGroup, props, emptyProps } from '@ngrx/store';

export const UsersActions = createActionGroup({
  source: 'Users',
  events: {
    'Load Users': emptyProps(),
    'Load Users Success': props<{ users: User[] }>(),
    'Load Users Failure': props<{ error: string }>(),
    'Delete User': props<{ id: string }>(),
  },
});

// Dispatch
this.store.dispatch(UsersActions.loadUsers());
this.store.dispatch(UsersActions.deleteUser({ id: '123' }));

Reducer

// users.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { UsersActions } from './users.actions';

export interface UsersState {
  items: User[];
  loading: boolean;
  error: string | null;
}

const initialState: UsersState = { items: [], loading: false, error: null };

export const usersReducer = createReducer(
  initialState,
  on(UsersActions.loadUsers, (state) => ({ ...state, loading: true, error: null })),
  on(UsersActions.loadUsersSuccess, (state, { users }) => ({ ...state, loading: false, items: users })),
  on(UsersActions.loadUsersFailure, (state, { error }) => ({ ...state, loading: false, error })),
  on(UsersActions.deleteUser, (state, { id }) => ({ ...state, items: state.items.filter((u) => u.id !== id) })),
);

Selectors

// users.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import type { UsersState } from './users.reducer';

export const selectUsersState = createFeatureSelector<UsersState>('users');

export const selectAllUsers = createSelector(
  selectUsersState,
  (state) => state.items
);

export const selectUserCount = createSelector(
  selectAllUsers,
  (users) => users.length
);

export const selectUserById = (id: string) => createSelector(
  selectAllUsers,
  (users) => users.find((u) => u.id === id)
);

Effects

// users.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { UsersActions } from './users.actions';

@Injectable()
export class UsersEffects {
  private actions$ = inject(Actions);
  private http = inject(HttpClient);

  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.loadUsers),
      switchMap(() =>
        this.http.get<User[]>('/api/users').pipe(
          map((users) => UsersActions.loadUsersSuccess({ users })),
          catchError((err) => of(UsersActions.loadUsersFailure({ error: err.message })))
        )
      )
    )
  );
}

Component usage

@Component({...})
export class UsersListComponent implements OnInit {
  private store = inject(Store);

  users = this.store.selectSignal(selectAllUsers);   // signal-based selector (NgRx 17+)
  count = this.store.selectSignal(selectUserCount);

  ngOnInit() {
    this.store.dispatch(UsersActions.loadUsers());
  }
}

store.selectSignal(selector) returns a Signal<T> — read with users() in template.

For Observable-based: users$ = this.store.select(selectAllUsers) + async pipe.

NgRx Component Store (per-component reactive store)

For feature-local state too small for full Store pattern:

import { ComponentStore } from '@ngrx/component-store';

interface PaginationState {
  page: number;
  pageSize: number;
  total: number;
}

@Injectable()
export class PaginationStore extends ComponentStore<PaginationState> {
  constructor() {
    super({ page: 1, pageSize: 10, total: 0 });
  }

  readonly page$ = this.select((s) => s.page);
  readonly totalPages$ = this.select((s) => Math.ceil(s.total / s.pageSize));

  readonly setPage = this.updater((state, page: number) => ({ ...state, page }));

  readonly loadPage = this.effect((trigger$: Observable<void>) =>
    trigger$.pipe(
      switchMap(() => this.api.fetchPage(this.get().page).pipe(
        tapResponse(
          (data) => this.patchState({ total: data.total }),
          (err) => console.error(err)
        )
      ))
    )
  );
}

// In component
providers: [PaginationStore]

Provided per-component (not providedIn: 'root') — unique instance.

NgRx Signals (newer, 2024+)

@ngrx/signals provides a signal-based store API. Less verbose than full Store + Effects:

import { signalStore, withState, withMethods, withComputed } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export const UsersStore = signalStore(
  { providedIn: 'root' },
  withState<{ items: User[]; loading: boolean }>({ items: [], loading: false }),
  withComputed((state) => ({
    count: computed(() => state.items().length),
  })),
  withMethods((state, http = inject(HttpClient)) => ({
    async loadUsers() {
      patchState(state, { loading: true });
      const items = await firstValueFrom(http.get<User[]>('/api/users'));
      patchState(state, { items, loading: false });
    },
  }))
);

// Use
const store = inject(UsersStore);
store.count();         // signal
store.loadUsers();     // method

Drop-in for many feature-state cases. Less boilerplate than full NgRx Store.

Anti-patterns

  • ❌ Subscribing to observables without unsubscription strategy — leaks memory.
  • ❌ Using effect() for derived state — use computed().
  • ❌ Reading this.someService.value() inside effect() then calling this.someService.update() from same effect — circular update warning.
  • ❌ Calling signal without parens (*ngIf="user" instead of *ngIf="user()") — common bug, doesn't unwrap.
  • ❌ Putting all state in NgRx Store when service+signals would do.
  • ❌ Subscribing to NgRx Store.select() then mutating result — selectors return immutable references.
  • ❌ Forgetting provideStore / provideEffects in standalone bootstrap — runtime errors.
  • ❌ Mixing observable + signal mental models in one feature without toSignal/toObservable bridges.
  • ❌ Using BehaviorSubject exposed publicly without .asObservable() — consumers can .next().
  • ❌ Manual Subject<void> cleanup pattern in Angular 16+ — use takeUntilDestroyed().

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

ASP.NET Core web framework conventions: Minimal API vs MVC controllers, Program.cs composition, DI lifetimes (Scoped/Singleton/Transient), Options pattern with IOptions<T>, middleware ordering (HTTPS redirect → routing → authentication → authorization), model binding and validation (FluentValidation / DataAnnotations), ProblemDetails error handling, structured logging with ILogger<T>, configuration layering (appsettings.json + environment variables + User Secrets), and health checks. Works alongside csharp-foundation:csharp-conventions and aspnet-core-plugin:efcore-patterns. Use this skill to: - Compose Program.cs correctly — register services, configure middleware in the right order, map endpoints. - Apply the Options pattern to avoid passing raw IConfiguration into services. - Write Minimal API endpoint groups with typed results and authorization. - Handle cross-cutting errors uniformly with ProblemDetails. - Configure structured logging and health checks for production readiness. Do NOT use this skill for: - EF Core entity configuration and migrations — see aspnet-core-plugin:efcore-patterns. - C# language idioms — see csharp-foundation:csharp-conventions. - Testing — see csharp-foundation:dotnet-testing.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する