Angular Signals یکی از مهم‌ترین قابلیت‌های Reactive در Angular است که برای مدیریت State و واکنش به تغییرات داده استفاده می‌شود.

با Signals می‌توان داده‌های متغیر برنامه را به‌صورت Reactive مدیریت کرد و Angular نیز می‌تواند تشخیص دهد کدام بخش از برنامه به آن داده وابسته است.

در این آموزش با signal، computed و effect آشنا می‌شویم و با مثال‌های واقعی بررسی می‌کنیم که هرکدام چه کاربردی دارند و چه زمانی باید از آن‌ها استفاده کنیم.

Angular Signals چیست؟

Signal در Angular یک مقدار Reactive است که هنگام تغییر می‌تواند مصرف‌کنندگان وابسته به خودش را از تغییر مطلع کند.

Signalها به شکل Function خوانده می‌شوند. یعنی برای دریافت مقدار یک Signal باید آن را فراخوانی کنید.

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

const count = signal(0);

console.log(count());

در این مثال مقدار اولیه Signal برابر با 0 است و با اجرای count() مقدار فعلی آن دریافت می‌شود.

چرا Angular Signals معرفی شد؟

در یک رابط کاربری مدرن، داده‌ها دائماً تغییر می‌کنند. تعداد محصولات سبد خرید، وضعیت Login کاربر، Theme، فیلترها و اطلاعات فرم نمونه‌هایی از State هستند.

Angular با Signals می‌تواند محل استفاده از State را Track کند و در زمان تغییر مقدار، بخش‌های وابسته را به‌روزرسانی کند.

این مدل Reactive باعث می‌شود مدیریت State محلی Componentها ساده‌تر و خواناتر شود.

ساخت Signal در Angular

برای ساخت Signal از تابع signal() استفاده می‌کنیم.

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

username = signal('Milad');

برای خواندن مقدار:

console.log(this.username());

خروجی:

Milad

Writable Signal چیست؟

Signal ساخته‌شده با signal() یک Writable Signal است. یعنی مقدار آن را می‌توان مستقیماً تغییر داد.

دو روش اصلی برای تغییر مقدار Writable Signal وجود دارد: set() و update().

تغییر مقدار Signal با set()

متد set() مقدار فعلی Signal را با مقدار جدید جایگزین می‌کند.

count = signal(0);

increase() {
  this.count.set(10);
}

پس از اجرای متد، مقدار Signal برابر با 10 خواهد شد.

تغییر مقدار Signal با update()

اگر مقدار جدید به مقدار قبلی وابسته باشد، استفاده از update() خواناتر است.

count = signal(0);

increase() {
  this.count.update(value => value + 1);
}

در این حالت Angular مقدار قبلی را در اختیار Callback قرار می‌دهد و مقدار جدید بر اساس آن محاسبه می‌شود.

تفاوت set و update در Angular Signals

روشکاربردنمونه
set()جایگزینی مستقیم مقدارcount.set(10)
update()محاسبه مقدار جدید بر اساس مقدار قبلیcount.update(v => v + 1)

استفاده از Signal در Template

برای نمایش مقدار Signal در Template نیز باید آن را مانند Function فراخوانی کنیم.

<p>Count: {{ count() }}</p>

اگر مقدار count تغییر کند، Angular وابستگی Template به این Signal را تشخیص می‌دهد و مقدار نمایش داده‌شده را به‌روزرسانی می‌کند.

مثال واقعی Counter با Angular Signals

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <h2>{{ count() }}</h2>

    <button (click)="increase()">
      Increase
    </button>

    <button (click)="decrease()">
      Decrease
    </button>
  `
})
export class CounterComponent {

  count = signal(0);

  increase() {
    this.count.update(value => value + 1);
  }

  decrease() {
    this.count.update(value => value - 1);
  }

}

در این مثال دیگر نیازی نیست برای تغییر UI کار اضافه‌ای انجام دهیم؛ Template مقدار Signal را می‌خواند و Angular تغییرات را Track می‌کند.

Computed در Angular چیست؟

computed() برای ساخت مقدارهایی استفاده می‌شود که از یک یا چند Signal دیگر محاسبه می‌شوند.

Computed Signal فقط Read-only است و مقدار آن را مستقیماً با set() یا update() تغییر نمی‌دهیم.

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

price = signal(100);
count = signal(2);

total = computed(() => this.price() * this.count());

مقدار total() در این مثال برابر با 200 است.

اگر price یا count تغییر کند، مقدار محاسبه‌شده نیز براساس Dependencyهای خود به‌روزرسانی می‌شود.

چرا از computed استفاده کنیم؟

هر زمانی که یک مقدار از Stateهای دیگر Derived می‌شود، معمولاً computed() انتخاب مناسبی است.

برای مثال قیمت نهایی، تعداد نتایج فیلترشده، وضعیت فعال بودن یک دکمه یا نام کامل کاربر می‌توانند Computed باشند.

Computed Signal چه تفاوتی با Signal معمولی دارد؟

ویژگیsignalcomputed
قابل تغییر مستقیمبلهخیر
set()داردندارد
update()داردندارد
Derived Stateمعمولاً خیربله
نوعWritableRead-only

Computed در Angular چگونه کار می‌کند؟

Computed Signalها به‌صورت Lazy ارزیابی می‌شوند؛ یعنی محاسبه تا زمانی که مقدار Computed واقعاً خوانده نشود انجام نمی‌شود.

مقدار محاسبه‌شده نیز Cache می‌شود و تا زمانی که Dependency مربوط به آن تغییر نکرده باشد، Angular می‌تواند همان مقدار قبلی را استفاده کند.

مثال واقعی computed برای سبد خرید

price = signal(250000);
quantity = signal(2);

totalPrice = computed(() => {
  return this.price() * this.quantity();
});

در Template:

<p>
  قیمت نهایی:
  {{ totalPrice() }}
</p>

اگر Quantity تغییر کند:

this.quantity.set(3);

مقدار totalPrice() نیز بر اساس مقدار جدید محاسبه خواهد شد.

مثال computed برای فیلتر کردن کاربران

users = signal([
  { id: 1, name: 'Ali', active: true },
  { id: 2, name: 'Sara', active: false },
  { id: 3, name: 'Reza', active: true }
]);

activeUsers = computed(() => {
  return this.users().filter(user => user.active);
});

در این حالت نیازی نیست یک State جداگانه برای کاربران فعال نگهداری کنیم. activeUsers مستقیماً از State اصلی مشتق می‌شود.

Effect در Angular چیست؟

effect() برای اجرای Side Effect در واکنش به تغییر Signalها استفاده می‌شود.

Effect حداقل یک بار اجرا می‌شود و هنگام اجرا Signalهایی را که خوانده می‌شوند Track می‌کند. اگر یکی از Dependencyهای Track‌شده تغییر کند، Effect دوباره اجرا خواهد شد.

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

theme = signal('light');

constructor() {

  effect(() => {
    console.log(this.theme());
  });

}

اگر مقدار Theme تغییر کند، Effect دوباره اجرا می‌شود.

Effect چه کاربردهایی دارد؟

Effect زمانی مناسب است که لازم باشد State Signal را با یک API غیر Reactive یا یک Side Effect هماهنگ کنیم.

برای مثال ذخیره Theme در LocalStorage، Logging، Analytics، کار با Canvas یا هماهنگ کردن State با برخی Libraryهای خارجی از کاربردهای مناسب Effect هستند.

مثال Effect با LocalStorage

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

theme = signal('light');

constructor() {

  effect(() => {
    localStorage.setItem(
      'theme',
      this.theme()
    );
  });

}

با تغییر Theme، مقدار جدید به LocalStorage نوشته می‌شود.

چه زمانی نباید از effect استفاده کنیم؟

یکی از اشتباهات رایج این است که از Effect برای کپی کردن مقدار یک Signal در Signal دیگر استفاده کنیم.

اگر یک مقدار مستقیماً از State دیگری مشتق می‌شود، معمولاً بهتر است از computed() استفاده کنیم.

روش نامناسب

price = signal(100);
quantity = signal(2);
total = signal(0);

constructor() {

  effect(() => {
    this.total.set(
      this.price() * this.quantity()
    );
  });

}

روش بهتر

price = signal(100);
quantity = signal(2);

total = computed(() => {
  return this.price() * this.quantity();
});

در این سناریو total یک Derived State است و نیازی به Effect ندارد.

signal یا computed یا effect؛ کدام را استفاده کنیم؟

APIکاربرد اصلیمثال
signal()نگهداری State قابل تغییرcount، theme، selectedItem
computed()ساخت Derived StatetotalPrice، filteredUsers
effect()اجرای Side EffectlocalStorage، logging، analytics

مثال واقعی signal، computed و effect کنار هم

در مثال زیر یک سبد خرید بسیار ساده داریم.

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <p>Price: {{ price() }}</p>

    <p>Quantity: {{ quantity() }}</p>

    <p>Total: {{ total() }}</p>

    <button (click)="increase()">
      Add Item
    </button>
  `
})
export class CartComponent {

  price = signal(500000);

  quantity = signal(1);

  total = computed(() =>
    this.price() * this.quantity()
  );

  constructor() {

    effect(() => {

      console.log(
        'Cart total:',
        this.total()
      );

    });

  }

  increase() {

    this.quantity.update(
      value => value + 1
    );

  }

}

در این مثال price و quantity State هستند، total یک Derived State است و Effect برای Logging تغییرات استفاده شده است.

Dependency Tracking در Angular Signals

Angular هنگام اجرای Computed یا Effect بررسی می‌کند چه Signalهایی خوانده شده‌اند و همان‌ها را به‌عنوان Dependency ثبت می‌کند.

یکی از نکات مهم این است که Dependencyها می‌توانند Dynamic باشند.

showCount = signal(false);
count = signal(10);

message = computed(() => {

  if (this.showCount()) {
    return `Count: ${this.count()}`;
  }

  return 'Count is hidden';

});

اگر showCount() برابر false باشد، ممکن است count() در آن اجرای Computed خوانده نشود و در نتیجه Dependency فعال آن Execution محسوب نشود.

untracked در Angular Signals چیست؟

گاهی داخل Effect یا Computed لازم است مقدار یک Signal را بخوانیم، اما نمی‌خواهیم آن Signal به Dependency تبدیل شود.

برای این کار می‌توان از untracked() استفاده کرد.

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

currentUser = signal('Milad');
counter = signal(0);

constructor() {

  effect(() => {

    console.log(
      'User:',
      this.currentUser()
    );

    console.log(
      'Counter:',
      untracked(this.counter)
    );

  });

}

در این مثال تغییر currentUser می‌تواند Effect را دوباره اجرا کند، اما خواندن Counter با untracked آن را به Dependency Effect تبدیل نمی‌کند.

Signal با Object و Array

Signal می‌تواند علاوه بر Number و String، Object و Array نیز نگهداری کند.

user = signal({
  name: 'Milad',
  age: 30
});

برای تغییر State بهتر است مقدار جدید تولید کنیم:

this.user.update(user => ({
  ...user,
  age: 31
}));

مثال Array با Signal

products = signal([
  { id: 1, title: 'Laptop' }
]);

addProduct() {

  this.products.update(products => [
    ...products,
    {
      id: 2,
      title: 'Keyboard'
    }
  ]);

}

readonly Signal چیست؟

گاهی می‌خواهیم یک Service بتواند State را تغییر دهد اما Componentهای دیگر فقط امکان خواندن آن را داشته باشند.

در این شرایط می‌توان یک نسخه Read-only از Signal ارائه کرد.

private count = signal(0);

readonly countValue =
  this.count.asReadonly();

Consumer می‌تواند countValue() را بخواند، اما متدهای set() و update() در اختیارش نیست.

استفاده از Signals در Service

Signals فقط مخصوص Component نیستند و می‌توان برای مدیریت State مشترک در Service نیز از آن‌ها استفاده کرد.

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

@Injectable({
  providedIn: 'root'
})
export class CartService {

  private items = signal<number[]>([]);

  readonly itemCount =
    computed(() => this.items().length);

  addItem(id: number) {

    this.items.update(items => [
      ...items,
      id
    ]);

  }

}

در این معماری Service منبع اصلی State است و Componentها فقط State موردنیاز را مصرف می‌کنند.

Angular Signals و Change Detection

وقتی یک Signal در Template یک Component خوانده می‌شود، Angular آن Signal را به‌عنوان Dependency آن Component Track می‌کند.

در صورت تغییر Signal، Angular می‌تواند Component وابسته را برای به‌روزرسانی در Change Detection علامت‌گذاری کند.

این رفتار با Componentهایی که از OnPush استفاده می‌کنند نیز یکپارچه است.

Signals یا RxJS؟

Signals و RxJS دقیقاً برای یک مسئله یکسان طراحی نشده‌اند و در پروژه‌های Angular می‌توانند در کنار هم استفاده شوند.

موضوعAngular SignalsRxJS
State محلی UIبسیار مناسبقابل استفاده
Derived StatecomputedOperators
Async Streamsهدف اصلی نیستبسیار مناسب
سادگیمعمولاً ساده‌ترمفاهیم بیشتری دارد
HTTPمی‌تواند در معماری State استفاده شودHttpClient همچنان Observable برمی‌گرداند

برای مثال می‌توانید داده HTTP را با RxJS دریافت کنید و State نهایی UI را با Signals مدیریت کنید.

آیا Signals جایگزین RxJS شده‌اند؟

خیر. Signals جایگزین کامل RxJS نیستند.

RxJS ابزار قدرتمندی برای Async Streamها، Event Streamها، ترکیب چند جریان داده و Operatorهایی مانند switchMap، debounceTime و combineLatest است.

Signals بیشتر برای Reactive State و Derived State داخل Angular مناسب هستند.

آیا Signals جایگزین NgRx می‌شوند؟

در پروژه‌های کوچک و متوسط، Signals می‌توانند بسیاری از نیازهای ساده State Management را بدون نیاز به Library خارجی پوشش دهند.

اما در پروژه‌های پیچیده که نیاز به معماری پیشرفته State، DevTools، Event-based State Management یا الگوهای مشخص سازمانی دارند، استفاده از ابزارهایی مانند NgRx همچنان می‌تواند منطقی باشد.

اشتباهات رایج در Angular Signals

استفاده بیش از حد از effect

Effect نباید انتخاب پیش‌فرض برای ایجاد Derived State باشد. برای مقادیری که از Signalهای دیگر ساخته می‌شوند، اول computed() را بررسی کنید.

نگهداری State تکراری

اگر مقدار قابل محاسبه از State موجود است، نگهداری یک Signal جداگانه برای آن ممکن است باعث ایجاد دو Source of Truth شود.

تغییر مستقیم Object

بهتر است هنگام تغییر Object و Array مقدار جدید ایجاد کنید تا جریان تغییر State شفاف و قابل پیش‌بینی باقی بماند.

استفاده از Effect برای زنجیره تغییر State

تغییر دادن چند Signal از داخل Effect می‌تواند Dependencyهای پیچیده و حتی چرخه‌های غیرضروری ایجاد کند. در این حالت بهتر است ساختار State و Derived State بازطراحی شود.

بهترین روش استفاده از Angular Signals

برای State اصلی که قابلیت تغییر دارد از signal() استفاده کنید.

برای مقادیری که از Stateهای دیگر محاسبه می‌شوند از computed() استفاده کنید.

برای Side Effectهایی مانند LocalStorage، Logging یا هماهنگی با APIهای غیر Reactive، در صورت نیاز از effect() استفاده کنید.

State را تا حد ممکن نزدیک به جایی نگهداری کنید که مورد استفاده قرار می‌گیرد و از ساخت Signalهای تکراری برای یک داده یکسان خودداری کنید.

سوالات متداول درباره Angular Signals

Angular Signal چیست؟

Signal یک مقدار Reactive در Angular است که تغییرات آن توسط مصرف‌کنندگان وابسته قابل Track است. مقدار Signal با فراخوانی آن مانند Function خوانده می‌شود.

چطور مقدار Signal را تغییر دهیم؟

برای مقداردهی مستقیم از set() و برای محاسبه مقدار جدید براساس مقدار قبلی از update() استفاده می‌شود.

computed در Angular چیست؟

Computed یک Signal Read-only است که مقدار خود را از Signalهای دیگر مشتق می‌کند. با تغییر Dependencyها، مقدار Derived نیز به‌روز می‌شود.

effect در Angular چیست؟

Effect عملیاتی است که Signalهای خوانده‌شده در اجرای خود را Track می‌کند و در واکنش به تغییر Dependencyهای مربوط دوباره اجرا می‌شود.

چه زمانی از effect استفاده کنیم؟

Effect بیشتر برای Side Effectهایی مانند Logging، Analytics، LocalStorage، Custom DOM behavior یا ارتباط با APIهای غیر Reactive مناسب است.

آیا برای محاسبه قیمت نهایی باید از effect استفاده کنیم؟

معمولاً خیر. اگر قیمت نهایی از قیمت و تعداد محصول محاسبه می‌شود، computed() انتخاب مناسب‌تری است.

آیا Angular Signals جایگزین RxJS شده‌اند؟

خیر. Signals برای Reactive State بسیار مناسب هستند، اما RxJS همچنان برای Streamهای Async و سناریوهای پیچیده Reactive کاربرد مهمی دارد.

آیا Signal فقط برای Component است؟

خیر. Signal را می‌توان در Component، Service و سایر بخش‌های مناسب برنامه Angular استفاده کرد.

آیا computed قابل تغییر است؟

خیر. Computed Signal به‌صورت Read-only است و متدهای set() و update() ندارد.

تفاوت signal و computed چیست؟

Signal معمولی برای نگهداری State قابل تغییر استفاده می‌شود، در حالی که Computed برای ساخت State مشتق‌شده از Signalهای دیگر کاربرد دارد.

جمع‌بندی

Angular Signals یک مدل ساده و قدرتمند برای مدیریت Reactive State در Angular فراهم می‌کنند.

اگر مقداری مستقیماً قابل تغییر است، معمولاً signal() انتخاب مناسبی است. اگر مقدار از Stateهای دیگر محاسبه می‌شود، از computed() استفاده کنید و اگر لازم است در واکنش به تغییر State یک Side Effect واقعی اجرا شود، effect() می‌تواند مورد استفاده قرار بگیرد.

درک تفاوت این سه API کمک می‌کند State Management پروژه‌های Angular ساده‌تر، خواناتر و قابل نگهداری‌تر شود.