# Angular signals

Angular Signals introduce a **fine‑grained reactivity** model that lets Angular know exactly which parts of your UI depend on which pieces of state. Instead of re‑running entire components or trees when “something changed”, Signals make updates targeted, predictable, and easier to reason about.

This article walks through Signals step‑by‑step, then ties everything together with a realistic example: a small “team tasks dashboard” that tracks users, filters, and derived counts using Signals.

---

## What are Angular Signals?

A **signal** is a wrapper around a value (primitive or object) that can notify Angular whenever that value changes.

* You **read** a signal by calling it like a function: `count()`.
    
* You **update** a writable signal using `set()` or `update()`.
    
* Angular tracks where signals are read so it can update only the affected consumers.
    

Conceptually:

```xml
tsconst count = signal(0);

console.log(count()); // read -> 0
count.set(1);         // write
console.log(count()); // read -> 1
```

Whenever `count()` is read inside a reactive context (for example, a template, `computed`, or `effect`), Angular remembers that relationship and re‑runs only the necessary code when `count` changes.

---

## Writable Signals: Your Source of Truth

**Writable signals** are your mutable state containers. You create them with `signal(initialValue)` and then mutate them using `set` and `update`.

```xml
tsimport { signal, WritableSignal } from '@angular/core';

const count: WritableSignal<number> = signal(0);

// Read
console.log('Count is', count());

// Write directly
count.set(3);

// Write based on previous value
count.update(value => value + 1);
```

## Real‑time example: Team filter state

Imagine a small task dashboard where you show tasks assigned to a selected team member:

```xml
tsinterface User {
  id: number;
  name: string;
}

interface Task {
  id: number;
  title: string;
  assignedTo: number;  // user id
  completed: boolean;
}

const users = signal<User[]>([
  { id: 1, name: 'Elmo' },
  { id: 2, name: 'Arya' },
]);

const tasks = signal<Task[]>([
  { id: 1, title: 'Fix login bug', assignedTo: 1, completed: false },
  { id: 2, title: 'Write docs',    assignedTo: 1, completed: true },
  { id: 3, title: 'Refactor API',  assignedTo: 2, completed: false },
]);

const selectedUserId = signal<number | null>(1);
```

Here:

* `users` and `tasks` are writable signals holding arrays.
    
* `selectedUserId` is a writable signal representing which user is currently “active” in the UI.
    

Updating the selection is trivial:

```xml
tsfunction selectUser(id: number | null) {
  selectedUserId.set(id);
}
```

Any part of the app that *reads* `selectedUserId()` in a reactive context will automatically update.

---

## Computed Signals: Derived, Read‑Only State

**Computed signals** are read‑only values derived from other signals. You define them using `computed(() => ...)`.

```xml
tsimport { computed, Signal } from '@angular/core';

const count = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
```

* `doubleCount()` always returns `count() * 2`.
    
* When `count` changes, Angular invalidates the cached value of `doubleCount`.
    
* The derivation is **lazy and memoized**: it only runs when someone reads `doubleCount()`.
    

You **cannot** write to a computed signal:

```xml
tsdoubleCount.set(3); // ❌ compile-time error
```

## Example: Selected user and task stats

Continuing our dashboard:

```xml
tsconst selectedUser = computed<User | null>(() => {
  const id = selectedUserId();
  return id == null ? null : users().find(u => u.id === id) ?? null;
});

const tasksForSelectedUser = computed<Task[]>(() => {
  const id = selectedUserId();
  if (id == null) return [];
  return tasks().filter(task => task.assignedTo === id);
});

const completedCount = computed<number>(() =>
  tasksForSelectedUser().filter(t => t.completed).length
);

const pendingCount = computed<number>(() =>
  tasksForSelectedUser().filter(t => !t.completed).length
);
```

Here:

* All four computed signals depend on lower‑level writable signals (`users`, `tasks`, `selectedUserId`).
    
* If `selectedUserId` changes, Angular knows it must recompute `selectedUser`, `tasksForSelectedUser`, `completedCount`, and `pendingCount`.
    
* If you add or update tasks, the counts update automatically.
    

Because computations are memoized, `tasks().filter(...)` only re‑runs when `tasks` or `selectedUserId` actually change, not on every render.

---

## Dynamic Dependencies: Branching Derivations

Computed signals only track **signals that are actually read** during a derivation. That means dependencies can change based on conditions.

```xml
tsconst showCount = signal(false);
const count = signal(0);

const conditionalCount = computed(() => {
  if (showCount()) {
    return `The count is ${count()}.`;
  } else {
    return 'Nothing to see here!';
  }
});
```

Behavior:

* If `showCount()` is `false` when `conditionalCount()` runs, it does **not** read `count()`, so `count` is not tracked as a dependency.
    
* Changing `count` will not trigger a recomputation until `showCount` becomes `true` and the `count()` branch is actually executed.
    
* If `showCount` flips back to `false`, `count` is again dropped as a dependency.
    

This makes complex UIs more efficient: inactive branches simply don’t participate in reactivity.

---

## Reactive Contexts: Where Angular Tracks Reads

A **reactive context** is a runtime situation where Angular watches which signals are being read. In those contexts, reads create dependencies.

Angular enters a reactive context when it:

* Executes an `effect` or `afterRenderEffect` callback.
    
* Evaluates a `computed` signal.
    
* Evaluates a `linkedSignal` or `resource` params/loader.
    
* Renders a component template (including host bindings).
    

Inside those contexts:

* The **consumer** is the running function or template.
    
* The **producer** is any signal you call.
    
* Angular wires them together so that producer changes re‑run the consumer.
    

## Example: Component using signals

```xml
ts@Component({
  selector: 'app-team-dashboard',
  standalone: true,
  template: `
    <section *ngIf="selectedUser(); else noUser">
      <h2>{{ selectedUser()?.name }}’s Tasks</h2>

      <p>
        Completed: {{ completedCount() }} |
        Pending: {{ pendingCount() }}
      </p>

      <ul>
        <li *ngFor="let task of tasksForSelectedUser()">
          <label>
            <input
              type="checkbox"
              [checked]="task.completed"
              (change)="toggleCompleted(task.id)"
            />
            {{ task.title }}
          </label>
        </li>
      </ul>
    </section>

    <ng-template #noUser>
      <p>Select a user to see their tasks.</p>
    </ng-template>

    <hr />

    <button *ngFor="let user of users()"
            (click)="selectUser(user.id)">
      {{ user.name }}
    </button>
    <button (click)="selectUser(null)">Clear selection</button>
  `,
})
export class TeamDashboardComponent {
  users = users;
  selectedUser = selectedUser;
  tasksForSelectedUser = tasksForSelectedUser;
  completedCount = completedCount;
  pendingCount = pendingCount;

  selectUser = selectUser;

  toggleCompleted(taskId: number) {
    tasks.update(current =>
      current.map(task =>
        task.id === taskId
          ? { ...task, completed: !task.completed }
          : task,
      ),
    );
  }
}
```

Here, the template is a **reactive context**:

* Every `signal()` call inside the bindings (`selectedUser()`, `tasksForSelectedUser()`, etc.) is tracked.
    
* When `selectedUserId` or `tasks` change, Angular only re‑renders this component, and only the bindings that depend on those signals are recomputed.
    

---

## Asserting “Not in a Reactive Context”

Sometimes you want to be sure certain logic **is not** running inside a reactive context—for example, to avoid subtle bugs when mixing subscriptions or manual state management.

Angular provides `assertNotInReactiveContext`:

```xml
tsimport { assertNotInReactiveContext } from '@angular/core';

function subscribeToEvents() {
  assertNotInReactiveContext(subscribeToEvents);
  // Safe to run subscription logic here
  externalEventSource.subscribe(...);
}
```

If `subscribeToEvents` is accidentally called from inside a reactive context, Angular throws a clear, targeted error (pointing to `subscribeToEvents`) instead of a vague reactive context error.

Use this guard when:

* You set up long‑lived subscriptions.
    
* You integrate with libraries that manage their own lifecycles.
    
* You know the call must be “one‑off” and not re‑run automatically.
    

---

## Reading Without Tracking: `untracked`

Sometimes you want to *peek* at a signal inside a reactive context without creating a dependency. Angular’s `untracked` helper does exactly that.

## Example: Logging incidental information

Suppose you have:

```xml
tsconst currentUser = signal<User | null>(null);
const counter = signal(0);
```

Naïve logging with an effect:

```xml
tseffect(() => {
  console.log(`User set to ${currentUser()} and counter is ${counter()}`);
});
```

This effect re‑runs when **either** `currentUser` or `counter` changes. But what if you only want it to re‑run when `currentUser` changes, and just show the *current* `counter` value at that time?

```xml
tsimport { effect, untracked } from '@angular/core';

effect(() => {
  console.log(
    `User set to ${currentUser()} and the counter is ${untracked(counter)}`
  );
});
```

Now:

* `currentUser()` is tracked as a dependency.
    
* `untracked(counter)` reads the signal but doesn’t register it as a dependency.
    

Updating `counter` alone will not re‑run the effect.

## Example: Calling external services without linking their reads

```xml
tseffect(() => {
  const user = currentUser();

  untracked(() => {
    // Even if loggingService internally reads signals,
    // they won’t become dependencies of this effect.
    this.loggingService.log(`User set to ${user?.name ?? 'anonymous'}`);
  });
});
```

`untracked(() => { ... })` temporarily disables dependency tracking for everything executed inside the callback.

---

## Putting It Together: A Mini “Live Team Dashboard”

Let’s combine everything into a more cohesive real‑world‑style example.

## Requirements

* Show a list of team members.
    
* Show each selected member’s tasks, with completed/pending stats.
    
* Log when a user changes, including a snapshot of counter state.
    
* Avoid unneeded recalculations and effects.
    

## State and Signals

```xml
tsimport {
  signal,
  WritableSignal,
  computed,
  effect,
  untracked,
  assertNotInReactiveContext,
} from '@angular/core';

interface User {
  id: number;
  name: string;
}

interface Task {
  id: number;
  title: string;
  assignedTo: number;
  completed: boolean;
}

const users = signal<User[]>([
  { id: 1, name: 'Elmo' },
  { id: 2, name: 'Arya' },
]);

const tasks = signal<Task[]>([
  { id: 1, title: 'Fix login bug',   assignedTo: 1, completed: false },
  { id: 2, title: 'Write docs',      assignedTo: 1, completed: true },
  { id: 3, title: 'Refactor API',    assignedTo: 2, completed: false },
]);

const selectedUserId: WritableSignal<number | null> = signal(1);
const counter = signal(0);
```

## Derived state

```xml
tsconst selectedUser = computed<User | null>(() => {
  const id = selectedUserId();
  return id == null ? null : users().find(u => u.id === id) ?? null;
});

const tasksForSelectedUser = computed<Task[]>(() => {
  const id = selectedUserId();
  if (id == null) return [];
  return tasks().filter(task => task.assignedTo === id);
});

const completedCount = computed<number>(() =>
  tasksForSelectedUser().filter(t => t.completed).length
);

const pendingCount = computed<number>(() =>
  tasksForSelectedUser().filter(t => !t.completed).length
);
```

## Side effect: log when `currentUser` changes

```typescript
tsconst currentUser = selectedUser;

effect(() => {
  const user = currentUser();          // tracked dependency
  const currentCounter = untracked(counter); // incidental read

  console.log(
    user
      ? `Switched to ${user.name}, counter snapshot: ${currentCounter}`
      : `No user selected, counter snapshot: ${currentCounter}`
  );
});
```

This effect:

* Re‑runs only when `selectedUserId` changes (because `currentUser()` is derived from it).
    
* Always uses the latest `counter` value, but changes to `counter` alone don’t trigger logs.
    

## Safe external subscription

Suppose you have a function that wires up WebSocket messages. You want to ensure it’s never called from a reactive context:

```typescript
tsfunction initWebsocketConnection() {
  assertNotInReactiveContext(initWebsocketConnection);
  // Connect once at app bootstrap, not during effects/templates
  this.socket.connect();
}
```

Call this from a non‑reactive place (e.g., main bootstrap or constructor of a root service).

---

## When to Reach for Signals in Your App

Angular Signals shine when:

* You need **fine‑grained performance**: dashboards, trading UIs, live metrics, or complex forms.
    
* You want a **predictable reactive model**: no hidden async zones, clear dependencies.
    
* You’re refactoring from RxJS‑heavy code and want smaller, easier‑to‑read pieces of state.
    

Use:

* **Writable signals** for local component or service state.
    
* **Computed signals** for anything derived: filters, counts, views of data.
    
* **Effects** for logging, network calls, and integration with non‑reactive APIs.
    
* `untracked` when you need incidental reads that should not drive re‑runs.
    
* `assertNotInReactiveContext` to guard code that must never be executed reactively.
    

By structuring your state as Signals and derivations, you get a clear mental model:  
**Sources → Derived values → Effects/UI**, with Angular handling the wiring and re‑execution automatically.

---
