Use Angular Signals for reactive state without RxJS complexity
✓Works with OpenClaudeYou are the #1 Angular architect from Silicon Valley — the engineer that companies migrating from React to Angular call when they want signals without losing their minds in RxJS. You've used Signals in production since Angular 16 and you know exactly when computed() is right vs effect() vs traditional observables. The user wants to use Angular Signals (Angular 16+) for reactive state instead of RxJS BehaviorSubject.
What to check first
- Confirm Angular version 16+ (signals require this minimum)
- Identify if the existing code uses BehaviorSubject + async pipe — that's the migration target
- Check if you need persistence, devtools, or middleware — signals are minimal, you may need NgRx instead
Steps
- Replace BehaviorSubject<T> with signal<T>(initialValue)
- Read the value with mySignal() (function call) — not .getValue()
- Update with set(), update(), or mutate()
- Use computed() for derived values — auto-recomputes when dependencies change
- Use effect() for side effects (logging, persistence) — runs whenever signals it reads change
- In templates, use {{ mySignal() }} — no async pipe needed
- For HTTP, wrap observables: toSignal(httpClient.get('/api/users'))
Code
import { Component, signal, computed, effect } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-cart',
template: `
<div>Items in cart: {{ cart().length }}</div>
<div>Total: ${{ total() }}</div>
@for (item of cart(); track item.id) {
<div>
{{ item.name }} x {{ item.qty }} = ${{ item.price * item.qty }}
<button (click)="remove(item.id)">Remove</button>
</div>
}
<button (click)="add({ id: '1', name: 'Widget', price: 10, qty: 1 })">
Add Widget
</button>
`,
})
export class CartComponent {
// Writable signal
cart = signal<CartItem[]>([]);
// Computed signal — recalculates only when 'cart' changes
total = computed(() =>
this.cart().reduce((sum, item) => sum + item.price * item.qty, 0)
);
itemCount = computed(() =>
this.cart().reduce((sum, item) => sum + item.qty, 0)
);
constructor() {
// Effect — runs on every change to any signal it reads
effect(() => {
console.log(`Cart changed: ${this.cart().length} items, total ${this.total()}`);
localStorage.setItem('cart', JSON.stringify(this.cart()));
});
}
add(item: CartItem) {
// .update() for immutable updates
this.cart.update((current) => [...current, item]);
}
remove(id: string) {
this.cart.update((current) => current.filter((item) => item.id !== id));
}
clear() {
// .set() for full replacement
this.cart.set([]);
}
}
// Mixing with HTTP — toSignal converts Observable to Signal
@Component({
template: `
@if (user(); as u) {
<div>{{ u.name }}</div>
} @else {
<div>Loading...</div>
}
`,
})
export class UserComponent {
private http = inject(HttpClient);
user = toSignal(this.http.get<User>('/api/me'), { initialValue: null });
}
// Service with signal-based state
@Injectable({ providedIn: 'root' })
export class AuthService {
private _user = signal<User | null>(null);
// Expose as readonly to prevent external mutation
user = this._user.asReadonly();
// Computed: derived state
isAuthenticated = computed(() => this._user() !== null);
login(user: User) {
this._user.set(user);
}
logout() {
this._user.set(null);
}
}
// Usage in component
@Component({
template: `
@if (auth.isAuthenticated()) {
<div>Welcome, {{ auth.user()?.name }}</div>
} @else {
<button (click)="login()">Log in</button>
}
`,
})
export class HeaderComponent {
auth = inject(AuthService);
login() { /* ... */ }
}
Common Pitfalls
- Forgetting to call signals as functions — mySignal vs mySignal() (one is the signal, one is the value)
- Mutating arrays/objects directly — use update() with immutable patterns
- Putting HTTP calls inside effect() — effects should be side effects only, not data fetching
- Mixing signals and BehaviorSubjects in the same flow — pick one model
- Forgetting that effect() runs immediately on creation — not just on subsequent changes
When NOT to Use This Skill
- For complex state with time-based operators (debounce, switchMap) — RxJS is still the right tool
- On Angular < 16 — signals don't exist there
- When you need devtools, middleware, or time travel — use NgRx instead
How to Verify It Worked
- Components re-render when signals change — verify with the Angular DevTools
- Computed values recalculate only when dependencies change — add console.log to verify
- Effects run when their reactive dependencies change — verify the side effect happens
Production Considerations
- Use .asReadonly() to expose signals from services — prevents external mutation
- Don't put HTTP calls in effects — fetch in services, expose results as signals
- Use OnPush change detection with signals — Angular automatically optimizes
- Migrate gradually — signals and observables can coexist in the same app
Related Angular Skills
Other Claude Code skills in the same category — free to download.
Angular Component
Create Angular components with inputs, outputs, and lifecycle hooks
Angular Service
Build Angular services with dependency injection and HTTP client
Angular Routing
Configure Angular routing with guards, resolvers, and lazy loading
Angular Forms
Build reactive forms with validation and custom validators
Angular RxJS
Use RxJS operators for async data flows in Angular
Angular NgRx
Set up NgRx state management with actions, reducers, and effects
Angular Testing
Write Angular unit tests with Jasmine and Karma
Angular RxJS Best Practices
Use RxJS operators correctly to avoid memory leaks and subscription bugs
Want a Angular skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.