Agent Skills by ALSEL
OpenAIソフトウェア開発⭐ リポ 1品質スコア 73/100

angular-signals

Angular v20以上において、シグナルベースのリアクティブな状態管理を実装します。signal()で反応性のある状態を作成し、computed()で派生状態を生成し、linkedSignal()で依存する状態を管理し、effect()で副作用を処理できます。状態管理に関する質問や、BehaviorSubject/Observableパターンからシグナルへの変更、またはリアクティブなデータフローの実装時に活用されます。

description の原文を見る

Implement signal-based reactive state management in Angular v20+. Use for creating reactive state with signal(), derived state with computed(), dependent state with linkedSignal(), and side effects with effect(). Triggers on state management questions, converting from BehaviorSubject/Observable patterns to signals, or implementing reactive data flows.

SKILL.md 本文

Angular Signals

シグナルは、状態管理のための Angular のリアクティブプリミティブです。同期的できめ細かなリアクティビティを提供します。

コアシグナル API

signal() - 書き込み可能な状態

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

// 書き込み可能なシグナルを作成
const count = signal(0);

// 値を読み取る
console.log(count()); // 0

// 新しい値を設定
count.set(5);

// 現在の値に基づいて更新
count.update(c => c + 1);

// 明示的な型指定
const user = signal<User | null>(null);
user.set({ id: 1, name: 'Alice' });

computed() - 派生状態

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

const firstName = signal('John');
const lastName = signal('Doe');

// 派生シグナル - 依存関係が変わると自動的に更新
const fullName = computed(() => `${firstName()} ${lastName()}`);

console.log(fullName()); // "John Doe"
firstName.set('Jane');
console.log(fullName()); // "Jane Doe"

// 複雑なロジックを含む computed
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() - リセット機能付き依存状態

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

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

// オプションが変わると最初のオプションにリセット
const selected = linkedSignal(() => options()[0]);

console.log(selected()); // "A"
selected.set('B'); // ユーザーが B を選択
console.log(selected()); // "B"
options.set(['X', 'Y']); // オプション変更
console.log(selected()); // "X" - 最初に自動リセット

// 前の値にアクセス
const items = signal<Item[]>([]);

const selectedItem = linkedSignal<Item[], Item | null>({
  source: () => items(),
  computation: (newItems, previous) => {
    // 選択がまだ存在する場合は保持を試みる
    const prevItem = previous?.value;
    if (prevItem && newItems.some(i => i.id === prevItem.id)) {
      return prevItem;
    }
    return newItems[0] ?? null;
  },
});

effect() - 副作用

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

@Component({...})
export class Search {
  query = signal('');

  constructor() {
    // クエリが変わると effect が実行
    effect(() => {
      console.log('Search query:', this.query());
    });

    // クリーンアップ付き effect
    effect((onCleanup) => {
      const timer = setInterval(() => {
        console.log('Current query:', this.query());
      }, 1000);

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

Effect のルール:

  • インジェクションコンテキスト内で実行(コンストラクタまたは runInInjectionContext で)
  • コンポーネント破棄時に自動的にクリーンアップされます

コンポーネント状態パターン

@Component({
  selector: 'app-todo-list',
  template: `
    <input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" />
    <button (click)="addTodo()" [disabled]="!canAdd()">追加</button>

    <ul>
      @for (todo of filteredTodos(); track todo.id) {
        <li [class.done]="todo.done">
          {{ todo.text }}
          <button (click)="toggleTodo(todo.id)">切り替え</button>
        </li>
      }
    </ul>

    <p>{{ remaining() }} 件残り</p>
  `,
})
export class TodoList {
  // 状態
  todos = signal<Todo[]>([]);
  newTodo = signal('');
  filter = signal<'all' | 'active' | 'done'>('all');

  // 派生状態
  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);

  // アクション
  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 相互運用

toSignal() - Observable からシグナルへ

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

@Component({...})
export class Timer {
  private http = inject(HttpClient);

  // Observable から - 初期値または allowUndefined が必要
  counter = toSignal(interval(1000), { initialValue: 0 });

  // HTTP から - ロード完了まで undefined
  users = toSignal(this.http.get<User[]>('/api/users'));

  // 同期 Observable (BehaviorSubject) の場合 requireSync を使用
  private user$ = new BehaviorSubject<User | null>(null);
  currentUser = toSignal(this.user$, { requireSync: true });
}

toObservable() - シグナルから Observable へ

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

@Component({...})
export class Search {
  query = signal('');

  private http = inject(HttpClient);

  // RxJS 演算子を使用するためにシグナルを Observable に変換
  results = toSignal(
    toObservable(this.query).pipe(
      debounceTime(300),
      switchMap(q => this.http.get<Result[]>(`/api/search?q=${q}`))
    ),
    { initialValue: [] }
  );
}

シグナル等価性

// カスタム等価性関数
const user = signal<User>({ id: 1, name: 'Alice' }, { equal: (a, b) => a.id === b.id });

// ID が変わった場合のみ更新をトリガー
user.set({ id: 1, name: 'Alice Updated' }); // 更新なし
user.set({ id: 2, name: 'Bob' }); // 更新をトリガー

未追跡の読み取り

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

const a = signal(1);
const b = signal(2);

// 'a' のみに依存し、'b' には依存しない
const result = computed(() => {
  const aVal = a();
  const bVal = untracked(() => b());
  return aVal + bVal;
});

サービス状態パターン

@Injectable({ providedIn: 'root' })
export class Auth {
  // プライベートな書き込み可能状態
  private _user = signal<User | null>(null);
  private _loading = signal(false);

  // パブリックな読み取り専用シグナル
  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);
  }
}

高度なパターン (resource() を含む) については、references/signal-patterns.md を参照してください。

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
plastikaweb
リポジトリ
plastikaweb/plastikspace
ライセンス
MIT
最終更新
2026/5/10

Source: https://github.com/plastikaweb/plastikspace / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: plastikaweb · plastikaweb/plastikspace · ライセンス: MIT