---
name: angular-effects-alternatives
description: This skill should be used when writing Angular code with signals and effects. Use when deciding whether to use effect(), computed(), or reactive patterns for state management. Applies to all Angular components and services using signals, especially when considering effect() for state propagation, data synchronization, or reactive flows. Essential for code review of effect usage and refactoring imperative patterns to declarative alternatives.
---

# Angular Effects Alternatives

## Overview

This skill guides proper usage of Angular's `effect()` and provides declarative alternatives for common patterns. Effects are frequently misused for state propagation, leading to circular updates, maintenance issues, and violations of reactive principles. This skill prevents anti-patterns and promotes maintainable, declarative code.

## When to Use Effects (Valid Use Cases)

Effects are **primarily for rendering content that cannot be rendered through data binding**. Valid use cases are limited to:

### 1. Logging
Recording application events or debugging:

```typescript
effect(() => {
  const error = this.error();
  if (error) {
    console.error('Error occurred:', error);
  }
});
```

### 2. Canvas Painting
Custom graphics rendering (e.g., Angular Three library, Chart.js integration):

```typescript
effect(() => {
  const data = this.chartData();
  this.renderCanvas(data);
});
```

### 3. Custom DOM Behavior
Imperative APIs that require direct DOM manipulation:

```typescript
effect(() => {
  const message = this.notificationMessage();
  if (message) {
    this.snackBar.open(message, 'Close');
  }
});
```

**Key principle:** Data binding is the preferred way to display data. Effects should only be used when data binding is insufficient.

## Understanding Auto-Tracking

Angular automatically tracks signals accessed during effect execution, **even within called methods**:

```typescript
effect(() => {
  this.logError(); // Signal tracking happens inside this method
});

logError(): void {
  const error = this.error(); // This signal is automatically tracked
  if (error) {
    console.error(error);
  }
}
```

**Implication:** Auto-tracking makes effect dependencies non-obvious and hard to maintain. This is a primary reason to avoid effects for state management.

## When NOT to Use Effects (Anti-Patterns)

### ❌ Anti-Pattern 1: State Propagation

**NEVER use effects to propagate state changes to other state:**

```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
  const value = this.source();
  this.derived.set(value * 2);
});
```

**Problems:**
- Risk of circular updates and infinite loops
- Hard to maintain due to implicit tracking
- Violates declarative reactive principles
- Inappropriate glitch-free semantics

### ❌ Anti-Pattern 2: Synchronizing Signals

```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
  const filter = this.filter();
  this.loadData(filter);
});
```

### ❌ Anti-Pattern 3: Event Emulation

```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
  const count = this.itemCount();
  this.countChanged.emit(count);
});
```

**Why signals ≠ events:** Signals are designed to be glitch-free, collapsing multiple updates. This makes them inappropriate for representing discrete events.

## Decision Tree: Effect vs Alternative

```
Need to react to signal changes?
│
├─ Synchronous derivation?
│  └─ ✅ Use computed()
│
├─ Asynchronous derivation?
│  └─ ✅ Use Resource API
│
├─ Complex reactive flow with race conditions?
│  └─ ✅ Use RxJS (toObservable + operators + toSignal)
│
├─ Event-based trigger (not state change)?
│  └─ ✅ React to event directly, not signal
│
├─ Need RxJS operators with signals?
│  └─ ✅ Use reactive helpers (rxMethod, deriveAsync)
│
└─ Rendering non-data-bound content (logging, canvas, imperative API)?
   └─ ✅ Use effect()
```

## Recommended Alternatives

### Alternative 1: Use `computed()` for Synchronous Derivations

**When to use:** Deriving new state synchronously from existing signals.

```typescript
// ✅ CORRECT - Declarative
const derived = computed(() => {
  return this.baseSignal() * 2;
});

const fullName = computed(() => {
  return `${this.firstName()} ${this.lastName()}`;
});
```

**Benefits:**
- Declarative and maintainable
- Automatic dependency tracking
- Memoized and efficient
- No risk of circular updates

### Alternative 2: Use Resource API for Asynchronous Derivations

**When to use:** Loading data based on reactive parameters.

```typescript
// ✅ CORRECT - Declarative async state
readonly itemsResource = resource({
  params: this.filter,
  loader: ({ params, abortSignal }) => {
    return this.dataService.load(params, abortSignal);
  }
});

readonly items = computed(() => this.itemsResource.value() ?? []);
```

**Benefits:**
- Automatic race condition handling
- Built-in loading/error states
- Declarative parameter tracking
- Cancellation support

**See also:** `ngrx-resource-api` skill for detailed Resource API patterns.

### Alternative 3: React to Events, Not State Changes

**When to use:** User interactions or DOM events should trigger actions.

```typescript
// ❌ WRONG - Reacting to signal change
effect(() => {
  const searchTerm = this.searchTerm();
  this.search(searchTerm);
});

// ✅ CORRECT - React to event
<input (input)="search($event.target.value)" />

// Component
search(term: string): void {
  this.searchTerm.set(term);
  this.performSearch(term);
}
```

**Benefits:**
- Clear causality (event → action)
- No auto-tracking complexity
- Explicit control flow

### Alternative 4: RxJS Integration

**When to use:** Complex reactive flows requiring operators like `debounceTime`, `switchMap`, `combineLatest`.

```typescript
// ✅ CORRECT - RxJS for complex flows
readonly searchTerm = signal('');
readonly searchTerm$ = toObservable(this.searchTerm);

readonly results$ = this.searchTerm$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.searchService.search(term))
);

readonly results = toSignal(this.results$, { initialValue: [] });
```

**Benefits:**
- Full RxJS operator ecosystem
- Race condition prevention (`switchMap`)
- Powerful composition
- Type-safe streams

**Pattern:** Signal → Observable (toObservable) → RxJS operators → Signal (toSignal)

### Alternative 5: Reactive Helpers

**When to use:** Need RxJS operators but prefer signal-centric API.

#### Using `rxMethod` (NgRx Signal Store)

```typescript
readonly loadItem = rxMethod<number>(
  pipe(
    tap(() => patchState(this, { loading: true })),
    switchMap(id => this.service.findById(id)),
    tap(item => patchState(this, { item, loading: false }))
  )
);

// Call with signal or value
constructor() {
  this.loadItem(this.selectedId);
}
```

#### Using `deriveAsync` (ngxtension)

```typescript
readonly data = deriveAsync(() => {
  const filter = this.filter();
  return this.service.load(filter);
});
```

**Benefits:**
- Signal-friendly API
- RxJS operator support
- Cleaner than manual Observable conversion
- Automatic subscription management

## The `explicitEffect` Consideration

Some libraries provide `explicitEffect` to restrict auto-tracking:

```typescript
// Uses untracked() internally to limit tracking
explicitEffect(this.id, (id) => {
  this.store.load(id);
});
```

**Evaluation:**
- ✅ Mitigates auto-tracking drawbacks
- ✅ Makes dependencies explicit
- ❌ Still imperative (not declarative)
- ❌ Doesn't solve circular update risks
- ❌ Less idiomatic than reactive alternatives

**Recommendation:** Prefer declarative patterns (computed, Resource API, RxJS) over `explicitEffect`.

## Common Refactoring Patterns

### Pattern 1: Effect for State Sync → computed()

```typescript
// ❌ Before
effect(() => {
  const x = this.x();
  const y = this.y();
  this.sum.set(x + y);
});

// ✅ After
readonly sum = computed(() => this.x() + this.y());
```

### Pattern 2: Effect for Async Load → Resource API

```typescript
// ❌ Before
effect(() => {
  const id = this.selectedId();
  this.loadItem(id);
});

// ✅ After
readonly itemResource = resource({
  params: this.selectedId,
  loader: ({ params }) => this.service.loadItem(params)
});
```

### Pattern 3: Effect for Debounced Search → RxJS

```typescript
// ❌ Before
effect(() => {
  const term = this.searchTerm();
  // No way to debounce within effect
  this.search(term);
});

// ✅ After
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results = toSignal(
  this.searchTerm$.pipe(
    debounceTime(300),
    switchMap(term => this.searchService.search(term))
  ),
  { initialValue: [] }
);
```

### Pattern 4: Effect for Event Notification → Direct Event Handling

```typescript
// ❌ Before
effect(() => {
  const value = this.value();
  this.valueChange.emit(value);
});

// ✅ After
updateValue(newValue: string): void {
  this.value.set(newValue);
  this.valueChange.emit(newValue);
}
```

## Code Review Checklist

When reviewing code with `effect()`, ask:

- [ ] Is this for rendering non-data-bound content? (logging, canvas, imperative APIs)
  - **YES:** Effect is appropriate
  - **NO:** Continue checklist

- [ ] Is this synchronous state derivation?
  - **YES:** Use `computed()` instead

- [ ] Is this asynchronous data loading?
  - **YES:** Use Resource API instead

- [ ] Does this need RxJS operators (debounce, switchMap, etc.)?
  - **YES:** Use RxJS integration or reactive helpers instead

- [ ] Is this reacting to a user event?
  - **YES:** Handle event directly instead

- [ ] Could this cause circular updates?
  - **YES:** Refactor immediately - this will cause bugs

## Anti-Pattern Detection Rules

Flag any effect that:

1. **Calls `set()` or `update()` on signals** - Likely state propagation anti-pattern
2. **Calls service methods that update state** - Hidden state propagation
3. **Emits events based on signal changes** - Signal/event semantic mismatch
4. **Has try/catch for async operations** - Should use Resource API
5. **Would benefit from debouncing/throttling** - Should use RxJS

## Migration Strategy

When converting effects to alternatives:

1. **Identify effect purpose** - State derivation? Async load? Event handling?
2. **Choose appropriate alternative** - Use decision tree above
3. **Implement replacement** - Follow patterns in this skill
4. **Test thoroughly** - Ensure reactive flow works correctly
5. **Remove effect** - Clean up unused code

## Key Principles

1. **Effects are for side effects, not state management**
2. **Prefer declarative over imperative**
3. **Use computed() for sync, Resource API for async**
4. **React to events, not state changes**
5. **RxJS for complex reactive flows**
6. **Auto-tracking is powerful but opaque - avoid when possible**

## When in Doubt

Ask: "Can the user see this without an effect using data binding?"
- **YES:** Don't use effect, use data binding
- **NO:** Effect might be appropriate (but verify against decision tree)
