angular-signals

Signal-based reactive state management for Angular v20+ with synchronous, fine-grained reactivity. Core APIs include signal() for writable state, computed() for derived state, linkedSignal() for dependent state with automatic reset, and effect() for side effects Integrates with RxJS via toSignal() and toObservable() for converting between observables and signals Supports custom equality functions, untracked reads to break dependencies, and read-only signal exposure via asReadonly() Typical patterns include component state management with filtered/derived data, service-level state with public read-only signals, and HTTP request handling with optional initial values

INSTALLATION
npx skills add https://github.com/analogjs/angular-skills --skill angular-signals
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

// Update based on current value

count.update(c => c + 1);

// With explicit type

const user = signal<User | null>(null);

user.set({ id: 1, name: 'Alice' });

### computed() - Derived State

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

const firstName = signal('John');

const lastName = signal('Doe');

// Derived signal - automatically updates when dependencies change

const fullName = computed(() => ${firstName()} ${lastName()});

console.log(fullName()); // "John Doe"

firstName.set('Jane');

console.log(fullName()); // "Jane Doe"

// Computed with complex logic

const items = signal<Item[]>([]);

const filter = signal('');

const filteredItems = computed(() => {

const query = filter().toLowerCase();

return items().filter(item =>

item.name.toLowerCase().includes(query)

);

});

const totalPrice = computed(() =>

filteredItems().reduce((sum, item) => sum + item.price, 0)

);


### linkedSignal() - Dependent State with Reset

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

const options = signal(['A', 'B', 'C']);

// Resets to first option when options change

const selected = linkedSignal(() => options()[0]);

console.log(selected()); // "A"

selected.set('B'); // User selects B

console.log(selected()); // "B"

options.set(['X', 'Y']); // Options change

console.log(selected()); // "X" - auto-reset to first

// With previous value access

const items = signal<Item[]>([]);

const selectedItem = linkedSignal<Item[], Item | null>({

source: () => items(),

computation: (newItems, previous) => {

// Try to preserve selection if item still exists

const prevItem = previous?.value;

if (prevItem &#x26;&#x26; newItems.some(i => i.id === prevItem.id)) {

return prevItem;

}

return newItems[0] ?? null;

},

});


### effect() - Side Effects

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

@Component({...})

export class Search {

query = signal('');

constructor() {

// Effect runs when query changes

effect(() => {

console.log('Search query:', this.query());

});

// Effect with cleanup

effect((onCleanup) => {

const timer = setInterval(() => {

console.log('Current query:', this.query());

}, 1000);

onCleanup(() => clearInterval(timer));

});

}

}


**Effect rules:**

- Run in injection context (constructor or with `runInInjectionContext`)

- Automatically cleaned up when component destroys

## Component State Pattern

@Component({

selector: 'app-todo-list',

template:

<input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" />

<button (click)="addTodo()" [disabled]="!canAdd()">Add</button>

<ul>

@for (todo of filteredTodos(); track todo.id) {

<li [class.done]="todo.done">

{{ todo.text }}

<button (click)="toggleTodo(todo.id)">Toggle</button>

</li>

}

</ul>

<p>{{ remaining() }} remaining</p>

,

})

export class TodoList {

// State

todos = signal<Todo[]>([]);

newTodo = signal('');

filter = signal<'all' | 'active' | 'done'>('all');

// Derived state

canAdd = computed(() => this.newTodo().trim().length > 0);

filteredTodos = computed(() => {

const todos = this.todos();

switch (this.filter()) {

case 'active': return todos.filter(t => !t.done);

case 'done': return todos.filter(t => t.done);

default: return todos;

}

});

remaining = computed(() =>

this.todos().filter(t => !t.done).length

);

// Actions

addTodo() {

const text = this.newTodo().trim();

if (text) {

this.todos.update(todos => [

...todos,

{ id: crypto.randomUUID(), text, done: false }

]);

this.newTodo.set('');

}

}

toggleTodo(id: string) {

this.todos.update(todos =>

todos.map(t => t.id === id ? { ...t, done: !t.done } : t)

);

}

}


## RxJS Interop

### toSignal() - Observable to Signal

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

import { interval } from 'rxjs';

@Component({...})

export class Timer {

private http = inject(HttpClient);

// From observable - requires initial value or allowUndefined

counter = toSignal(interval(1000), { initialValue: 0 });

// From HTTP - undefined until loaded

users = toSignal(this.http.get<User[]>('/api/users'));

// With requireSync for synchronous observables (BehaviorSubject)

private user$ = new BehaviorSubject<User | null>(null);

currentUser = toSignal(this.user$, { requireSync: true });

}


### toObservable() - Signal to Observable

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

import { switchMap, debounceTime } from 'rxjs';

@Component({...})

export class Search {

query = signal('');

private http = inject(HttpClient);

// Convert signal to observable for RxJS operators

results = toSignal(

toObservable(this.query).pipe(

debounceTime(300),

switchMap(q => this.http.get<Result[]>(/api/search?q=${q}))

),

{ initialValue: [] }

);

}


## Signal Equality

// Custom equality function

const user = signal<User>(

{ id: 1, name: 'Alice' },

{ equal: (a, b) => a.id === b.id }

);

// Only triggers updates when ID changes

user.set({ id: 1, name: 'Alice Updated' }); // No update

user.set({ id: 2, name: 'Bob' }); // Triggers update


## Untracked Reads

import { untracked } from '@angular/core';

const a = signal(1);

const b = signal(2);

// Only depends on 'a', not 'b'

const result = computed(() => {

const aVal = a();

const bVal = untracked(() => b());

return aVal + bVal;

});


## Service State Pattern

@Injectable({ providedIn: 'root' })

export class Auth {

// Private writable state

private _user = signal<User | null>(null);

private _loading = signal(false);

// Public read-only signals

readonly user = this._user.asReadonly();

readonly loading = this._loading.asReadonly();

readonly isAuthenticated = computed(() => this._user() !== null);

private http = inject(HttpClient);

async login(credentials: Credentials): Promise<void> {

this._loading.set(true);

try {

const user = await firstValueFrom(

this.http.post<User>('/api/login', credentials)

);

this._user.set(user);

} finally {

this._loading.set(false);

}

}

logout(): void {

this._user.set(null);

}

}

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card