Angular في 2026 (معلومات مُتحقَّق منها في أبريل 2026، عرضة للتطوّر) يقترح نموذجين تفاعليّين جنباً إلى جنب: الـ signals (حديثة، بسيطة، متزامنة) و RxJS (قويّ، غير متزامن، ناضج). عدم الخلط بينهما، ومعرفة متى يُستخدَم كلّ واحد، هو مفتاح كتابة كود Angular حديث وفعّال. هذا الدليل يُفكّك كلّ نموذج، وقواه، وتفاعلاته، والأنماط التي تشتغل فعلاً.
وصول الـ signals غيّر بعمق الممارسة في Angular. قبل 2024، كان RxJS حاضراً في كلّ مكان: BehaviorSubject لمشاركة الحالة، الـ async pipe في كلّ القوالب، عوامل التركيب للحسابات المشتقّة. هذا المنهج كان قويّاً لكنّه يتطلّب إتقاناً حقيقياً لـ RxJS، ممّا كان يُبطئ إدماج المطوّرين الجدد ويُنتج أحياناً كوداً مُفرط التفاعلية. الـ signals تبسّط بشكل ملحوظ الحالات المتزامنة، تاركةً لـ RxJS سيطرته على غير المتزامن. هذا التقسيم الجديد للعمل يجعل Angular أكثر قابلية للوصول دون التضحية بالقوّة التي بنت سمعته.
للسياق الأوسع → Angular للمؤسسات: دليل عملي.
المحتويات
- Signals مقابل RxJS: تقسيم العمل
- Signals: اللبنات الثلاث
- الأنماط الشائعة مع signals
- RxJS: العوامل التي تحسب فعلاً
- HTTP مع RxJS
- التداخل: toSignal و toObservable
- المزالق الكلاسيكيّة
- الأداء و change detection
- أسئلة شائعة
1. Signals مقابل RxJS: تقسيم العمل
المنعكس الصحيح في 2026: ابدأ بالـ signals، انزل إلى RxJS حين تكون الحاجة فعليّة.
Signals لـ:
- حالة واجهة متزامنة (عدّاد، نمط تحرير، قيم مُرشّحات)
- بيانات مشتقّة قابلة للحساب بصورة متزامنة
- حالة المكوّن
- التواصل parent-enfant عبر inputs/outputs بصيغة signals
- حالة مُشترَكة بين المكوّنات عبر خدمات تكشف signals
RxJS لـ:
- طلبات HTTP (HttpClient يُرجع Observables)
- أحداث DOM أو WebSocket
- تركيبات غير متزامنة معقّدة (debounce، throttle، retry، switchMap)
- تدفّقات زمنية (intervals، timers)
- backpressure وإدارة التدفّق
كثير من الحالات التي كنّا نُعالجها بـ RxJS قبل 2024 (subjects لمشاركة الحالة) صارت اليوم أبسط مع signals. الـ Observables تبقى مهمّة لما هو غير متزامن ومتواصل فعلاً.
2. Signals: اللبنات الثلاث
signal()
حاوية قيمة تفاعليّة.
import { signal } from "@angular/core";
const count = signal(0);
count(); // قراءة: تُرجع 0
count.set(5); // كتابة مباشرة
count.update(c => c + 1); // كتابة مبنيّة على القيمة السابقة
داخل قالب:
<p>العدّاد: {{ count() }}</p>
استدعاء count() يقرأ القيمة. إذا تغيّرت، Angular يُعيد رسم جزء القالب الذي يعتمد عليها تلقائياً، دون شيء آخر يفعل أكثر من اللازم.
computed()
قيمة مشتقّة. تُحسَب من جديد تلقائياً حين تتغيّر التبعيّات. مع تذكّر: إن قرأتها عدّة مرّات دون تغيّر التبعيّات، فلا يُعاد الحساب.
const items = signal<Item[]>([]);
const search = signal("");
const filtered = computed(() =>
items().filter(item => item.nom.includes(search()))
);
// داخل القالب
@for (item of filtered(); track item.id) {
<li>{{ item.nom }}</li>
}
الـ filtered يُعاد حسابه فقط عند تغيّر items أو search. لا حساب زائد.
effect()
يُنفّذ كوداً عند كلّ تغيّر للـ signals المُستخدَمة. مفيد للآثار الجانبيّة (logging، مزامنة localStorage، استدعاءات أمريّة).
import { effect } from "@angular/core";
@Component(...)
export class MyComponent {
user = signal<User | null>(null);
constructor() {
effect(() => {
const u = this.user();
if (u) {
console.log(`المستخدم: ${u.nom}`);
localStorage.setItem("lastUser", JSON.stringify(u));
}
});
}
}
يُنظَّف الـ effect تلقائياً عند تدمير المكوّن (إذا استُدعي في سياق حقن). لا حاجة لـ ngOnDestroy.
3. الأنماط الشائعة مع signals
حالة مُشترَكة عبر خدمة
@Injectable({ providedIn: "root" })
export class CartService {
items = signal<CartItem[]>([]);
count = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, i) => sum + i.prix * i.quantite, 0)
);
add(item: CartItem) {
this.items.update(list => [...list, item]);
}
remove(id: string) {
this.items.update(list => list.filter(i => i.id !== id));
}
clear() {
this.items.set([]);
}
}
عدّة مكوّنات يمكنها حقن CartService وقراءة signalsه. أيّ تعديل يُنتشر تلقائياً. منهج أبسط بكثير من Redux أو NgRx لحالة معتدلة التعقيد.
Inputs بصيغة signals
import { input } from "@angular/core";
@Component({
selector: "app-card",
standalone: true,
template: `<div>{{ titre() }}</div>`,
})
export class CardComponent {
titre = input.required<string>();
variant = input<"default" | "primary">("default");
}
الـ input() (Angular 17.1+) ينشئ signal للقراءة فقط. أحدث من @Input(). يسمح باستخدام computed() الذي يعتمد على inputs دون اللجوء إلى ngOnChanges. كود أنظف، وأقلّ سطحاً للخطأ.
Outputs
import { output } from "@angular/core";
@Component(...)
export class CardComponent {
clicked = output<string>();
onClick() {
this.clicked.emit("card-1");
}
}
الدالّة output() تحلّ محلّ @Output() new EventEmitter(). أبسط، type-safe.
two-way binding مع model
import { model } from "@angular/core";
@Component({
selector: "app-input",
template: `<input [value]="value()" (input)="value.set($event.target.value)" />`,
})
export class InputComponent {
value = model.required<string>();
}
يسمح للأب بكتابة [(value)]="userInput". مزامنة في الاتّجاهين تلقائياً، ممّا يبسّط بشكل ملحوظ مكوّنات النماذج المخصّصة بتجنّب boilerplate الـ EventEmitter اليدوي.
4. RxJS: العوامل التي تحسب فعلاً
لا حاجة لمعرفة الـ 200 عامل في RxJS. حوالي عشرين يغطّون 95% من الحالات.
التحويل
map(fn): يحوّل كلّ قيمةfilter(predicate): يُرشّح القيمtap(fn): أثر جانبيّ دون تعديل القيمة (للـ debug، log)
التحكّم في غير المتزامن
switchMap(fn): يستبدل بـ Observable جديد، يُلغي السابق. مثاليّ للـ HTTP يُلغي الطلب السابقmergeMap(fn): ينفّذ بالتوازي، لا يحفظ الترتيبconcatMap(fn): ينفّذ تسلسليّاً، يحفظ الترتيبexhaustMap(fn): يتجاهل الانبعاثات الجديدة طالما السابقة لم تنته
الزمنيّ
debounceTime(ms): يُصدر فقط بعد N ميلّي ثانية من الخمودthrottleTime(ms): يُصدر مرّة واحدة كلّ N ميلّي ثانية كحدّ أقصىdelay(ms): يؤخّر كلّ الانبعاثات
التركيب
combineLatest([a$, b$]): يُصدر حين تكون كلّ المصادر قد أصدرت مرّة على الأقلّ، ثمّ عند كلّ تغيّرforkJoin([a$, b$]): مكافئ Promise.all، ينتظر كلّ المصادر، يُصدر مرّة واحدةmerge(a$, b$): يجمع عدّة مصادر
الترشيح
distinctUntilChanged(): يُصدر فقط إذا تغيّرت القيمةtake(n): يأخذ الـ N الأوائل ثمّ يُكملtakeUntil(notifier$): يأخذ حتى يُصدر مصدر آخر
الأخطاء
catchError(fn): يستبدل خطأً بقيمة أو Observable آخرretry(n): يُعيد المحاولة N مرّات عند الخطأ
نمط استعمال نموذجيّ
this.searchInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.api.search(query).pipe(
catchError(() => of([])),
)),
).subscribe(results => this.results.set(results));
هذا النمط (debounce + switchMap + catchError) من أكثرها فائدة. يصعب استنساخه بأناقة دون RxJS. يُلغي الطلبات المتقادمة حين يكتب المستخدم بسرعة، ويتعامل مع الأخطاء برشاقة.
5. HTTP مع RxJS
الـ HttpClient يُرجع Observables.
import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: "root" })
export class ClientService {
private http = inject(HttpClient);
getClients() {
return this.http.get<Client[]>("/api/clients");
}
searchClients(query: string) {
return this.http.get<Client[]>("/api/clients", {
params: { q: query },
});
}
}
لماذا Observables بدل Promises؟
- قابلة للإلغاء: إذا فعلت
unsubscribe، الطلب يُلغى فعلاً (مفيد معswitchMap) - قابلة للتأليف: يمكن سَلسَلة عوامل أخرى (retry، catchError، transform)
- multicast ممكن مع
share
الاستعمال في مكوّن
@Component({
selector: "app-clients",
standalone: true,
imports: [CommonModule],
template: `
@if (clients()) {
<ul>
@for (c of clients()!; track c.id) {
<li>{{ c.nom }}</li>
}
</ul>
} @else {
<p>جارٍ التحميل...</p>
}
`,
})
export class ClientsComponent {
private clientService = inject(ClientService);
clients = toSignal(this.clientService.getClients());
}
الـ toSignal يحوّل Observable مباشرة إلى Signal قابل للاستعمال في القالب. لا اشتراك يدويّ، لا إلغاء اشتراك، لا async pipe.
6. التداخل: toSignal و toObservable
import { toSignal, toObservable } from "@angular/core/rxjs-interop";
// Observable إلى Signal
const count$ = interval(1000);
const count = toSignal(count$, { initialValue: 0 });
// Signal إلى Observable
const value = signal(0);
const value$ = toObservable(value);
// نمط مُركَّب: signal -> HTTP بـ debounce -> signal
const search = signal("");
const results = toSignal(
toObservable(search).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.api.search(q)),
),
{ initialValue: [] },
);
هذا النمط يجمع بساطة signals (حالة الواجهة) مع قوّة RxJS (التدفّق غير المتزامن). النتيجة signal نهائيّ قابل للاستهلاك مباشرة في القالب.
takeUntilDestroyed
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component(...)
export class MyComponent {
ngOnInit() {
this.someObservable$.pipe(
takeUntilDestroyed(),
).subscribe(...);
}
}
يُلغي الاشتراك تلقائياً عند تدمير المكوّن. يحلّ محلّ الأنماط اليدويّة بـ Subject و takeUntil.
7. المزالق الكلاسيكيّة
التعديل (mutation) بدل الاستبدال
// خاطئ: Angular لا يكتشف التغيير
const items = signal<Item[]>([]);
items().push(newItem); // mutation، لا إشعار
// صحيح: مرجع جديد
items.update(list => [...list, newItem]);
استعمل دائماً set() أو update() بمرجع جديد. الـ signals تكشف التغيير بالمرجع لا بمحتوى المصفوفة.
Effects بشكل متسلسل
const a = signal(0);
const b = signal(0);
effect(() => {
if (a() > 5) b.set(a() * 2); // يغيّر b داخل effect
});
// حلقة لا نهائيّة ممكنة إذا أطلق b تغييراً لـ a في مكان آخر
يسمح Angular بذلك افتراضياً، لكن جعل التغييرات صريحة يحسّن القراءة. لحساب b بناءً على a: استعمل computed() بدل effect. الـ computed مُحصَّن ضدّ هذا النوع من الحلقات.
Subscriptions منسيّة
// خاطئ: اشتراك لم يُنظَّف
this.observable$.subscribe(...);
// صحيح
this.observable$.pipe(takeUntilDestroyed()).subscribe(...);
// أو
const sub = this.observable$.subscribe(...);
ngOnDestroy() { sub.unsubscribe(); }
switchMap مقابل mergeMap
// خاطئ: إذا نقر المستخدم بسرعة، عدّة طلبات بالتوازي، race conditions
this.click$.pipe(
mergeMap(() => this.api.save()),
).subscribe();
// صحيح: يُلغي الطلبات السابقة
this.click$.pipe(
switchMap(() => this.api.save()),
).subscribe();
// لكن: لعمليّة لا نريد إلغاءها (مثلاً دفع)، استعمل exhaustMap
this.click$.pipe(
exhaustMap(() => this.api.pay()),
).subscribe();
8. الأداء و change detection
مع provideZonelessChangeDetection()، Angular لم يَعد يُجري change detection بعد كلّ حدث غير متزامن، بل فقط عند تغيّر signal. الأداء يرتفع، خصوصاً في واجهات بكثير من المؤقّتات.
OnPush change detection
للمكوّنات التي لا تستعمل signals بعد:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
المكوّن لا يُعاد رسمه إلّا إذا تغيّرت inputsه (بالمرجع) أو نُودي يدويّاً على markForCheck.
Signal-based مع OnPush
التركيب الأمثل: مكوّنات بصيغة signals + OnPush. إعادة رسم فقط عند تغيّر الـ signals المُستخدَمة. هذا هو الوضع الافتراضي المثاليّ لـ 2026.
راجع تحسين أداء Angular.
9. أسئلة شائعة
هل يجب نقل كلّ RxJS إلى signals؟
لا. RxJS يبقى أفضل للتدفّقات غير المتزامنة. انقل ما هو متزامن (BehaviorSubject لمشاركة حالة بين المكوّنات إلى signal). أبقِ RxJS لـ HTTP والأحداث والتدفّقات المعقّدة.
Subject هل استُبدل بـ signal؟
لمشاركة قيمة بين المكوّنات: نعم، signal أبسط. لإطلاق أحداث ظرفيّة (نقر، ناقل رسائل): Subject يبقى مهمّاً. لتدفّق observable: Subject أو Observable.
هل inputs بصيغة signals إلزاميّة؟
لا، @Input() الكلاسيكيّة لا تزال تعمل. لكن input() (signal) مُوصى به للمكوّنات الجديدة: تكتيب أفضل، اندماج مع computed()، لا حاجة لـ ngOnChanges للاستجابة للتغييرات.
كيف نُشارك signal بين مكوّنين شقيقين؟
عبر خدمة مُحقَنة في المكوّنين. الخدمة تكشف signal. المكوّنان يقرآن و/أو يكتبان. أبسط من نظام تواصل parent-enfant على عدّة مستويات.
هل نمط zoneless جاهز للإنتاج؟
نعم منذ Angular 18+. يُجمع مع مكوّنات قائمة كلّياً على signals للاستفادة الكاملة. المكوّنات القديمة بـ @Input الكلاسيكيّة يمكنها التعايش لكنّها لا تستفيد بنفس القدر من مكاسب الأداء.
كيف أُنقّح إعادات الرسم غير المتوقّعة؟
Angular DevTools (امتداد Chrome/Firefox) يُظهر المكوّنات التي تُعاد رسمها، inputsها، حالتها. مفيد جدّاً لكشف الرسوم الزائدة وفهم انتشار change detection.
متى forkJoin بدل combineLatest؟
forkJoin: نريد انبعاثاً واحداً، حين تنتهي كلّ المصادر (مكافئ Promise.all). combineLatest: نريد انبعاثاً عند كلّ تغيّر، مع الاحتفاظ بآخر قيمة من كلّ مصدر. لـ HTTP منعزل: forkJoin. لمصادر تواصل البثّ: combineLatest.
مقالات مرتبطة (سلسلة Angular)
- Angular للمؤسسات: دليل عملي (الدليل الرئيسي)
- معمارية Angular المعياريّة
- تحسين أداء Angular