اكتشف كيف تغير Angular Signals قواعد اللعبة في إدارة الحالة، مع شرح تقني عميق وأمثلة عملية تكشف أسرار الأداء خلف الكواليس وتجنب الفخاخ الشائعة في تطبيقات العالم الحقيقي.
الـ Signals في Angular تعتمد على مفهوم بسيط ولكنه قوي: كل قيمة متغيرة تصبح كائناً ذكياً يعرف من يعتمد عليه وكيفية إعلامه عند التغيير. عندما تنشئ signal باستخدام الدالة signal()، فأنت في الواقع تنشئ كائناً يحتوي على القيمة الحالية بالإضافة إلى قائمة بالمراقبين (consumers) الذين يحتاجون إلى معرفة متى تتغير هذه القيمة.
لفهم هذا بشكل أفضل، تخيل أنك في اجتماع عمل، ولديك لوحة بيضاء تحتوي على معلومات مهمة. بدلاً من أن تسأل كل شخص في الغرفة عن رأيه في المعلومات كل خمس دقائق، تخبر الأشخاص الذين يهتمون بهذه المعلومات بأن يرفعوا أيديهم عندما تتغير. هكذا تعمل الـ Signals: بدلاً من إعادة فحص جميع المكونات عند كل تغيير، تخبر Angular بالضبط أي المكونات تحتاج إلى التحديث عند تغيير قيمة معينة.
// تشريح Signal من الداخل (مبسطة)
class Signal<T> {
private _value: T;
private _c new Set<() => void>();
constructor(initialValue: T) {
this._value = initialValue;
}
get value(): T {
// عندما يقرأ شخص ما القيمة، يضاف إلى قائمة المراقبين
const currentConsumer = getCurrentConsumer();
if (currentConsumer) {
this._consumers.add(currentConsumer);
}
return this._value;
}
set value(newValue: T) {
if (this._value !== newValue) {
this._value = newValue;
// إعلام جميع المراقبين بأن القيمة تغيرت
this._consumers.forEach(consumer => consumer());
}
}
}
// في Angular، يتم إدارة هذا بشكل أكثر كفاءة باستخدام خوارزميات متقدمة
// لتجنب إعادة الحسابات غير الضروريةأحد الجوانب المذهلة في الـ Signals هو أنها تعمل بشكل متزامن مع نظام Zone.js الحالي. هذا يعني أنه يمكنك البدء باستخدام الـ Signals في جزء صغير من التطبيق دون الحاجة لإعادة كتابة كل شيء. عندما تستخدم signal داخل قالب مكون، يقوم Angular تلقائياً بإضافة هذا المكون إلى قائمة المراقبين للقيمة، وعندما تتغير القيمة، يقوم Angular بتحديث المكون فقط دون الحاجة لإعادة فحص التطبيق بالكامل.
إذا كانت الـ Signals العادية هي الأساس، فإن الـ computed signals هي الطبقة الذكية التي تجعل النظام قوياً حقاً. الـ computed signal هي قيمة مشتقة من signals أخرى، لكنها ذكية بما يكفي لإعادة الحساب فقط عندما تتغير إحدى القيم التي تعتمد عليها. هذا يشبه كثيراً الدوال البحتة (pure functions) في البرمجة الوظيفية، لكن مع إضافة نظام تتبع التبعيات تلقائياً.
// مثال عملي على computed signals
import { signal, computed } from '@angular/core';
@Component({
selector: 'app-cart',
template: `
<div>Total: {{ total() }}</div>
<div>Discounted: {{ discountedTotal() }}</div>
<div>Tax: {{ tax() }}</div>
<div>Final: {{ finalAmount() }}</div>
`
})
export class CartComponent {
// Signals الأصلية
items = signal<Product[]>([]);
discount = signal<number>(0);
taxRate = signal<number>(0.15);
// Computed signals
total = computed(() => {
return this.items().reduce((sum, item) => sum + item.price, 0);
});
discountedTotal = computed(() => {
return this.total() * (1 - this.discount());
});
tax = computed(() => {
return this.discountedTotal() * this.taxRate();
});
finalAmount = computed(() => {
return this.discountedTotal() + this.tax();
});
addItem(item: Product) {
this.items.update(items => [...items, item]);
}
}
// عندما نضيف منتجاً جديداً، يتم تحديث total فقط
// ثم يتم تحديث discountedTotal وtax وfinalAmount تلقائياً
// ولكن فقط إذا كانت قيمتها النهائية ستتغير بالفعلما يحدث خلف الكواليس هنا مذهل حقاً. عندما تنشئ computed signal، يقوم Angular بإنشاء رسم بياني للتبعيات (dependency graph) بين جميع الـ signals. عندما تتغير قيمة signal ما، يقوم Angular بتتبع جميع الـ computed signals التي تعتمد عليها، ويحدد بالضبط أي منها يحتاج إلى إعادة الحساب. هذا يعني أنه إذا كان لديك computed signal معقدة تعتمد على 10 signals أخرى، لكنها في النهاية لا تتغير قيمتها، فلن يتم تحديث أي مكون يعتمد عليها. هذا النوع من التحسين كان مستحيلاً تقريباً مع النظام القديم.
الـ Effects في Angular Signals هي الآلية التي تسمح لك بتنفيذ إجراءات جانبية (side effects) عندما تتغير قيمة signal. هذا يشبه كثيراً الـ useEffect في React، لكنه أكثر قوة لأنه يفهم السياق الكامل للـ signals التي يعتمد عليها. الـ Effects مفيدة بشكل خاص للتفاعلات مع العالم الخارجي مثل الـ API calls، تخزين البيانات في localStorage، أو حتى التعامل مع مكتبات الطرف الثالث التي لا تفهم الـ Signals.
في أحد المشاريع الأخيرة، استخدمنا الـ Effects لتنفيذ نظام مزامنة ذكي بين واجهة المستخدم والـ backend. عندما يغير المستخدم إعدادات معينة، نقوم بتحديث signal محلية أولاً، ثم يقوم effect بإرسال التغيير إلى السيرفر. إذا فشل الطلب، نقوم تلقائياً بإعادة تعيين الـ signal إلى قيمتها السابقة. هذا النوع من السلوك كان يتطلب سابقاً مكتبة كاملة لإدارة الحالة مثل NgRx، لكن مع الـ Signals أصبح ممكناً بكود بسيط ومباشر.
// مثال عملي على استخدام Effects مع API calls
import { signal, effect } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-settings',
template: `
<input [(ngModel)]="username()" placeholder="Username">
<div *ngIf="syncStatus() === 'syncing'">Syncing...</div>
<div *ngIf="syncStatus() === 'error'">Error saving settings</div>
`
})
export class SettingsComponent {
username = signal<string>('');
syncStatus = signal<'idle' | 'syncing' | 'error'>('idle');
constructor(private http: HttpClient) {
// Effect لمزامنة الاسم مع السيرفر
effect((onCleanup) => {
const currentValue = this.username();
this.syncStatus.set('syncing');
const timeoutId = setTimeout(() => {
this.http.post('/api/settings', { username: currentValue })
.subscribe({
next: () => this.syncStatus.set('idle'),
error: () => {
this.syncStatus.set('error');
// إعادة تعيين القيمة إلى آخر قيمة ناجحة
this.username.set(localStorage.getItem('lastUsername') || '');
}
});
}, 500); // debounce
onCleanup(() => clearTimeout(timeoutId));
});
// تحميل القيمة الأولية من localStorage
const savedUsername = localStorage.getItem('lastUsername');
if (savedUsername) {
this.username.set(savedUsername);
}
}
}
// لاحظ كيف يتعامل الـ effect مع:
// 1. الـ debounce لمنع إرسال طلبات كثيرة
// 2. إعادة تعيين القيمة عند الفشل
// 3. تنظيف الـ timeout عند تدمير المكونأحد الجوانب المهمة في الـ Effects هو أنها توفر آلية تنظيف تلقائية. عندما يتم تدمير المكون، يتم تلقائياً إلغاء جميع الـ Effects المرتبطة به. هذا يمنع مشاكل الذاكرة الشائعة مثل الـ memory leaks التي كانت تحدث عندما ننسى إلغاء الاشتراكات في RxJS. أيضاً، الـ Effects تفهم متى يجب إعادة التشغيل بناءً على التغييرات في الـ signals التي تعتمد عليها، مما يجعلها أكثر كفاءة من الحلول التقليدية.
الكلام عن الأداء بدون أرقام هو مجرد ثرثرة. لذلك قمت بعمل اختبار عملي على تطبيق حقيقي يحتوي على قائمة طويلة من العناصر القابلة للتصفية والفرز. قارنت بين ثلاث طرق لإدارة الحالة: الطريقة التقليدية مع Zone.js، الطريقة مع OnPush، والطريقة مع الـ Signals. النتائج كانت صادمة:
الفرق الكبير يأتي من أن الـ Signals تسمح لـ Angular بتحديث DOM بدقة جراحية. بدلاً من إعادة فحص جميع المكونات، تقوم Angular بتحديث العناصر الدقيقة التي تغيرت فقط. هذا يشبه كثيراً الفرق بين إعادة طلاء حائط كامل عندما تريد تغيير لون مربع صغير عليه، وبين تغيير لون المربع فقط.
لكن هناك حالات لا تكون فيها الـ Signals أسرع بالضرورة. مثلاً، إذا كان لديك مكون بسيط جداً يحتوي على عدد قليل من العناصر، قد لا تلاحظ فرقاً كبيراً. أيضاً، إذا كنت تعتمد بشكل كبير على RxJS في تطبيقك الحالي، قد لا يكون التحول إلى الـ Signals مفيداً على الفور. لكن في التطبيقات الكبيرة والمعقدة، الفرق في الأداء يكون واضحاً جداً، خاصة إذا كنت تعاني من مشاكل في الـ Change Detection.
رغم أن الـ Signals رائعة، إلا أنها ليست الحل السحري لكل المشاكل. هناك حالات قد تكون فيها الطرق التقليدية أفضل:
أيضاً، يجب أن تكون حذراً عند مزج الـ Signals مع RxJS. على سبيل المثال، تحويل signal إلى observable ثم العودة إلى signal مرة أخرى قد يسبب مشاكل في الأداء وإدارة الذاكرة. في مثل هذه الحالات، من الأفضل اختيار أحد النظامين والالتزام به قدر الإمكان.
خلال عملي مع الـ Signals في مشاريع حقيقية، وقعت في بعض الفخاخ التي قد تواجهها أنت أيضاً. إليك أهمها وكيفية تجنبها:
من السهل جداً إنشاء تبعية دائرية بين الـ computed signals دون أن تلاحظ. مثلاً:
// مثال على تبعية دائرية - لا تفعل هذا!
const a = computed(() => b() + 1);
const b = computed(() => a() * 2);
// سيؤدي هذا إلى خطأ في وقت التشغيل
// Error: Detected cycle in computationsالحل هو إعادة تصميم الـ computed signals بحيث لا تعتمد على بعضها البعض بشكل دائري. إذا وجدت نفسك في هذا الموقف، فهذا غالباً علامة على أنك بحاجة لإعادة التفكير في بنية البيانات الخاصة بك.
الـ computed signals يجب أن تكون دوالاً بحتة (pure functions) لا تحدث أي تغييرات في الحالة. إذا قمت بتحديث signal داخل computed signal، قد تواجه سلوكاً غير متوقع:
// خطأ شائع - لا تفعل هذا!
const total = computed(() => {
const newTotal = calculateTotal();
this.items.set([]); // تعديل signal داخل computed!
return newTotal;
});
// هذا قد يسبب مشاكل في تتبع التبعيات وإعادة الحسابالحل هو فصل الحسابات عن التحديثات. قم بحساب القيمة أولاً، ثم قم بالتحديث في مكان آخر إذا لزم الأمر.
أحد الأخطاء الشائعة للمطورين الجدد مع الـ Signals هو نسيان استدعاء الـ signal كدالة عند استخدامها في الكود العادي:
// خطأ شائع
const username = signal('John');
console.log(username); // سيطبع Signal object، ليس القيمة!
// الطريقة الصحيحة
console.log(username()); // يطبع 'John'هذا الخطأ قد يسبب مشاكل صعبة التشخيص، خاصة عندما تمرر الـ signal إلى دوال أو مكتبات لا تتوقع أن تستقبل كائن signal بدلاً من القيمة الفعلية.
رغم أن الـ Effects توفر آلية تنظيف تلقائية، إلا أنه من الممكن إنشاء memory leaks إذا لم تكن حذراً. مثلاً، إذا أنشأت اشتراكاً في RxJS داخل effect ولم تقم بإلغائه:
// قد يسبب memory leak
const data = signal<Data | null>(null);
effect(() => {
if (data()) {
// مشكلة: لا يتم إلغاء الاشتراك عند تدمير المكون
interval(1000).subscribe(() => {
console.log('This will keep running even after component is destroyed!');
});
}
});
// الحل: استخدم onCleanup
effect((onCleanup) => {
if (data()) {
const sub = interval(1000).subscribe(() => {
console.log('This will be cleaned up automatically');
});
onCleanup(() => sub.unsubscribe());
}
});الـ Signals ليست مجرد إضافة جديدة لـ Angular، بل هي بداية لتحول كبير في كيفية بناء التطبيقات باستخدام الإطار. في مؤتمر Angular Nation الأخير، كشف الفريق عن خطط لجعل الـ Signals الأساس الجديد لإدارة الحالة في Angular، مع خطط لجعلها متوافقة بشكل أفضل مع الـ RxJS والـ Zone.js في المستقبل.
أحد التطورات المثيرة التي نتوقعها هو دمج الـ Signals مع الـ Router وHTTP Client لجعل إدارة البيانات أكثر سلاسة. تخيل مثلاً أن يكون لديك signal تمثل بيانات المستخدم، وعندما ينتقل المستخدم إلى صفحة جديدة، يتم تلقائياً تحميل البيانات المطلوبة لتلك الصفحة وتحديث الـ signal دون الحاجة لكتابة كود معقد لإدارة الحالة.
أيضاً، نتوقع أن نرى مكتبات إدارة حالة جديدة مبنية على الـ Signals، مشابهة لـ NgRx ولكن أبسط وأكثر كفاءة. في الواقع، بعض المكتبات مثل NgRx Signal Store موجودة بالفعل وتقدم طريقة جديدة تماماً لإدارة الحالة المعقدة باستخدام الـ Signals كأساس.
لكن الأهم من كل هذا هو التغيير في طريقة تفكيرنا كمطورين. الـ Signals تعلمنا التفكير في إدارة الحالة بشكل أكثر دقة وكفاءة، بدلاً من الاعتماد على الحلول العامة التي قد تكون مكلفة في الأداء. هذا النوع من التفكير سيجعلنا مطورين أفضل، بغض النظر عن الإطار أو المكتبة التي نستخدمها.
إذا كنت تريد البدء مع Angular Signals اليوم، إليك نصائحي العملية بناءً على تجربتي:
الـ Signals ليست مجرد ميزة جديدة، بل هي إعادة تعريف لكيفية بناء التطبيقات في Angular. إذا كنت تعمل على تطبيق كبير وتعاني من مشاكل الأداء أو تعقيد إدارة الحالة، فإن الـ Signals تستحق التجربة. ابدأ اليوم بمكون واحد، وشاهد الفرق بنفسك. صدقني، بمجرد أن تعتاد على الدقة والكفاءة التي توفرها الـ Signals، ستجد صعوبة في العودة إلى الطرق القديمة.