Angular Signals ليست مجرد ميزة جديدة، بل إعادة تعريف لكيفية تفاعل المكونات مع البيانات في الواجهة. اكتشف كيف تعمل تحت الغطاء، لماذا ستتخلص من OnPush، وكيف تبني تطبيقات أسرع وأكثر استقراراً من اليوم الأول.
في آخر تحديث لـ Angular 16، ظهرت ميزة لم تلفت انتباه الكثيرين في البداية، لكنها اليوم تُحدث ضجة حقيقية في مجتمع المطورين: Angular Signals. لماذا؟ لأنها ببساطة تُعيد كتابة قواعد اللعبة في إدارة الحالة داخل التطبيقات. تخيل أنك تستطيع الآن تغيير قيمة متغير واحد، فيُحدث تحديثاً دقيقاً ودقيقاً جداً في الواجهة دون الحاجة لإعادة رسم المكون بالكامل أو حتى استخدام OnPush. هذا ليس مجرد تحسين، بل ثورة في كيفية تفكيرنا في التفاعل بين البيانات والواجهة.
في هذا المقال، لن نتحدث عن ماهية Signals بطريقة نظرية جافة. بدلاً من ذلك، سنغوص في أعماقها التقنية، ونرى كيف تعمل خلف الكواليس، وكيف يمكنها أن تحل مشاكل حقيقية واجهناها جميعاً في مشاريع الإنتاج. سنبدأ بفهم المشكلة التي جاءت Signals لحلها، ثم ننتقل إلى كيفية تطبيقها عملياً، وأخيراً سنناقش الفخاخ التي قد تقع فيها وكيف تتجنبها. إذا كنت تريد أن تكون مستعداً للموجة القادمة في تطوير Angular، فهذا هو الوقت المناسب للغوص.
منذ سنوات، اعتدنا على استخدام Change Detection في Angular لإعلام الواجهة بالتغييرات في البيانات. لكن هذه الآلية، رغم قوتها، كانت تعاني من مشكلة أساسية: كانت تعمل بنمط شامل. أي تغيير في أي جزء من التطبيق كان يؤدي إلى إعادة فحص كامل لشجرة المكونات، حتى لو كان التغيير بسيطاً جداً. هذا يعني أن التطبيقات الكبيرة كانت تعاني من بطء ملحوظ، خاصة عند التعامل مع قوائم كبيرة أو بيانات متغيرة بشكل متكرر.
حاولنا حل هذه المشكلة باستخدام ChangeDetectionStrategy.OnPush، الذي قلل من نطاق الفحص إلى المكونات التي تلقت مدخلات جديدة فقط. لكن هذا الحل جاء مع تحدياته الخاصة: كان علينا إدارة التغييرات يدوياً باستخدام markForCheck()، وكان من السهل جداً نسيان ذلك أو ارتكاب أخطاء تؤدي إلى واجهة لا تُحدث بشكل صحيح. بالإضافة إلى ذلك، كان التعامل مع البيانات المتداخلة أو المتغيرة بشكل غير مباشر (مثل البيانات المشتقة) يتطلب الكثير من الكود الإضافي والحذر الشديد.
هنا تأتي Angular Signals لتقدم حلاً جذرياً. بدلاً من الاعتماد على نظام Change Detection التقليدي، تعتمد Signals على مفهوم الـ Reactivity الدقيق. هذا يعني أن أي تغيير في قيمة Signal يؤدي إلى تحديث فقط الأجزاء التي تعتمد على هذه القيمة في الواجهة، دون الحاجة لإعادة فحص شجرة المكونات بالكامل. هذا ليس مجرد تحسين للأداء، بل تغيير في الفلسفة برمتها لكيفية إدارة الحالة في التطبيقات
// مثال تقليدي مع OnPush
@Component({
selector: 'app-counter',
template: `{{ count }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
this.cdr.markForCheck(); // يجب تذكر هذا دائماً!
}
constructor(private cdr: ChangeDetectorRef) {}
}
// نفس المثال باستخدام Signals
@Component({
selector: 'app-counter-signals',
template: `{{ count() }}`,
standalone: true
})
export class CounterSignalsComponent {
count = signal(0); // لا حاجة لـ OnPush أو markForCheck
increment() {
this.count.update(v => v + 1); // الواجهة تُحدث تلقائياً ودون فحص شامل
}
}لفهم قوة Angular Signals، يجب أن نفهم كيف تعمل داخلياً. عندما تنشئ Signal باستخدام signal()، فإنك في الواقع تنشئ كائناً يحتفظ بقيمة داخلية ويوفر طرقاً لتحديث هذه القيمة (set، update، mutate). لكن الجزء الذكي هنا هو أن هذا الكائن يحتفظ أيضاً بقائمة من الـ Subscribers، أي الأجزاء التي تعتمد على هذه القيمة.
عندما تقرأ قيمة Signal داخل قالب المكون باستخدام count()، يقوم Angular بتسجيل هذا القالب كSubscriber لهذه الـ Signal. وعندما تتغير قيمة الـ Signal، يقوم Angular بإعلام جميع الـ Subscribers بالتغيير، مما يؤدي إلى تحديث الجزء المحدد من القالب فقط. هذا يعني أن التغييرات تحدث بدقة جراحية، دون الحاجة لإعادة رسم المكون بالكامل أو حتى تشغيل دورة Change Detection كاملة.
لكن كيف يدير Angular هذه العلاقة بين الـ Signals والـ Subscribers دون التأثير على الأداء؟ هنا يأتي دور مفهوم الـ Effect. الـ Effect هو دالة تُنفذ تلقائياً عندما تتغير أي من الـ Signals التي تعتمد عليها. على سبيل المثال، إذا كان لديك effect يتتبع قيمة signal معينة، فإنه سيُنفذ فقط عندما تتغير هذه القيمة، وليس عند كل دورة Change Detection. هذا يجعل الـ Effects مثالية لتنفيذ عمليات جانبية تعتمد على تغيرات الحالة، مثل تسجيل البيانات أو إرسال طلبات API.
// مثال على استخدام effect لمراقبة تغييرات Signal
@Component({
selector: 'app-user-profile',
template: `{{ user().name }}`,
standalone: true
})
export class UserProfileComponent {
user = signal<User>({ name: 'Ahmed', age: 30 });
constructor() {
effect(() => {
console.log('User changed:', this.user()); // يُنفذ فقط عند تغيير user
// يمكنك هنا إرسال طلب API أو تسجيل البيانات
});
}
updateName(newName: string) {
this.user.update(u => ({ ...u, name: newName }));
}
}من المهم أن نفهم أن الـ Effects لا تُنفذ بشكل متزامن مع تغيير الـ Signal. بدلاً من ذلك، يتم جدولة تنفيذها في نهاية دورة الـ Event Loop الحالية، مما يضمن عدم حظر واجهة المستخدم أو التأثير على أداء التطبيق. هذا مشابه لكيفية عمل setTimeout مع تأخير صفر، لكنه مُدار داخلياً بواسطة Angular لضمان الأداء الأمثل.
أحد أقوى ميزات Angular Signals هو القدرة على إنشاء بيانات مشتقة باستخدام computed. تخيل أنك لديك قائمة من المنتجات وترغب في عرض عدد المنتجات التي في المخزون فقط. في السابق، كان عليك إنشاء خاصية جديدة في المكون، وتحديثها يدوياً عند كل تغيير في القائمة الأصلية، مع الحرص على استخدام OnPush وmarkForCheck. مع computed، يمكنك ببساطة تعريف هذه القيمة المشتقة، وسيتولى Angular كل شيء تلقائياً.
الـ computed Signals ليست مجرد اختصار للكود، بل هي أداة قوية لتحسين الأداء. لأنها تعتمد فقط على الـ Signals التي تستخدمها، فإنها تُحدث قيمتها فقط عند تغيير أي من هذه الـ Signals. هذا يعني أنك لست بحاجة للقلق بشأن إعادة حساب القيم المشتقة بشكل غير ضروري، مما يقلل من الحمل على المعالج ويحسن من أداء التطبيق.
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products()">
{{ product.name }} - {{ product.price | currency }}
</div>
<div>Total in stock: {{ totalInStock() }}</div>
`,
standalone: true
})
export class ProductListComponent {
products = signal<Product[]>([
{ name: 'Laptop', price: 1000, inStock: true },
{ name: 'Phone', price: 500, inStock: false },
{ name: 'Tablet', price: 300, inStock: true }
]);
// computed signal: يُحدث تلقائياً عند تغيير products
totalInStock = computed(() =>
this.products().filter(p => p.inStock).length
);
addProduct(product: Product) {
this.products.update(p => [...p, product]);
// totalInStock يُحدث تلقائياً!
}
}لكن هناك نقطة مهمة يجب الانتباه إليها: الـ computed Signals تُعيد حساب قيمتها فقط عند تغيير الـ Signals التي تعتمد عليها مباشرة. إذا كان لديك computed يعتمد على computed آخر، فإن Angular سيتتبع هذه العلاقة تلقائياً ويحدث القيم بالترتيب الصحيح. ومع ذلك، يجب أن تكون حذراً من إنشاء سلاسل طويلة من الـ computed Signals، لأنها قد تؤدي إلى تحديثات غير ضرورية إذا لم تكن العلاقات بينها واضحة.
على الرغم من قوة Angular Signals، فإنها ليست بديلاً كاملاً لـ RxJS. في الواقع، تم تصميم Signals للعمل جنباً إلى جنب مع RxJS، مما يوفر لك أفضل ما في العالمين: الـ Reactivity الدقيق لـ Signals مع قوة معالجة البيانات المتدفقة لـ RxJS. يمكنك بسهولة تحويل Observable إلى Signal باستخدام toSignal، والعكس باستخدام toObservable، مما يتيح لك الاستفادة من كلتا التقنيتين في نفس التطبيق.
على سبيل المثال، إذا كان لديك Observable يستقبل بيانات من API، يمكنك تحويله إلى Signal باستخدام toSignal، ثم استخدام هذه الـ Signal داخل قوالب المكونات. هذا يعني أنك لن تحتاج إلى التعامل مع الـ async pipe في القوالب، مما يجعل الكود أكثر نظافة وسهولة في القراءة. بالإضافة إلى ذلك، يمكنك استخدام الـ Signals داخل الـ Observables باستخدام fromSignal، مما يتيح لك دمج البيانات المتزامنة وغير المتزامنة بسهولة.
@Component({
selector: 'app-user-dashboard',
template: `
@if (user()) {
<div>{{ user()!.name }}</div>
<div>Last login: {{ lastLogin() | date }}</div>
} @else {
<div>Loading...</div>
}
`,
standalone: true
})
export class UserDashboardComponent {
private userService = inject(UserService);
// تحويل Observable إلى Signal
user = toSignal(this.userService.getCurrentUser(), {
initialValue: null
});
// computed signal يعتمد على Signal من Observable
lastLogin = computed(() => {
const u = this.user();
return u ? new Date(u.lastLogin) : null;
});
}لكن هناك نقطة مهمة يجب الانتباه إليها عند استخدام toSignal: إذا لم توفر قيمة أولية، فإن الـ Signal ستكون undefined حتى يُصدر الـ Observable قيمة. هذا قد يؤدي إلى أخطاء إذا حاولت قراءة الـ Signal قبل أن تكون جاهزة. لحل هذه المشكلة، يمكنك إما توفير قيمة أولية، أو استخدام الـ @if directive في القالب للتحقق من وجود القيمة قبل استخدامها.
القرار بين استخدام RxJS وSignals ليس قراراً إما/أو، بل يتعلق بنوع المشكلة التي تحاول حلها. استخدم RxJS عندما: - تحتاج إلى معالجة بيانات متدفقة أو أحداث متكررة (مثل مدخلات المستخدم أو تدفقات WebSocket) - تحتاج إلى دمج أو تحويل بيانات من مصادر متعددة - تريد الاستفادة من الـ Operators القوية مثل debounceTime، switchMap، أو combineLatest استخدم Signals عندما: - تريد إدارة حالة بسيطة داخل المكون - تحتاج إلى تحديثات دقيقة في الواجهة دون إعادة رسم المكون بالكامل - تريد تبسيط الكود باستخدام computed للبيانات المشتقة - تعمل مع بيانات متزامنة تحتاج إلى تحديثات فورية في الواجهة
في الواقع، أفضل الممارسات هي استخدام كلتا التقنيتين معاً. على سبيل المثال، يمكنك استخدام RxJS لجلب البيانات من API ومعالجتها، ثم تحويل النتيجة إلى Signal لعرضها في الواجهة. هذا يمنحك قوة معالجة البيانات لـ RxJS مع بساطة إدارة الحالة لـ Signals.
رغم قوة Angular Signals، هناك بعض الفخاخ التي قد يقع فيها المطورون الجدد. أحد أكثر هذه الفخاخ شيوعاً هو نسيان أن الـ Signals ليست مجرد متغيرات عادية. عندما تقرأ قيمة Signal باستخدام count()، فإنك في الواقع تنفذ دالة، وليس مجرد قراءة قيمة. هذا يعني أنك لا تستطيع استخدام الـ Signals مباشرة في بعض السياقات التي تتوقع قيم ثابتة، مثل داخل الـ for loops التقليدية أو كقيم في الـ switch statements.
// ❌ خطأ شائع: استخدام Signal مباشرة في for loop
for (let i = 0; i < this.items().length; i++) {
// هذا لن يعمل كما تتوقع!
console.log(this.items()[i]);
}
// ✅ الحل الصحيح: تخزين القيمة في متغير مؤقت
const items = this.items();
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}فخ آخر شائع هو محاولة تعديل الـ Signals داخل الـ computed. تذكر أن الـ computed يجب أن تكون نقية، أي أنها لا يجب أن تُحدث أي حالة خارجية. إذا حاولت تعديل Signal داخل computed، فقد تواجه سلوكاً غير متوقع أو أخطاء في وقت التشغيل. بدلاً من ذلك، استخدم effect إذا كنت بحاجة إلى تنفيذ منطق جانبي عند تغيير البيانات.
// ❌ خطأ: تعديل Signal داخل computed
fullName = computed(() => {
const first = this.firstName();
const last = this.lastName();
this.fullNameSignal.set(`${first} ${last}`); // ❌ لا تفعل هذا!
return `${first} ${last}`;
});
// ✅ الحل الصحيح: استخدم effect بدلاً من computed
constructor() {
effect(() => {
this.fullNameSignal.set(`${this.firstName()} ${this.lastName()}`);
});
}أخيراً، كن حذراً عند استخدام الـ Signals مع الـ OnPush. على الرغم من أن Signals تعمل بشكل جيد مع OnPush، إلا أنك قد تواجه مشاكل إذا حاولت دمج الاثنين بشكل غير صحيح. على سبيل المثال، إذا كان لديك مكون يستخدم OnPush ويعتمد على Signal، فقد لا يُحدث المكون بشكل صحيح إذا لم يتم تغيير مدخلاته. الحل هو إما استخدام Signals فقط، أو التأكد من أن جميع التغييرات في الحالة تُحدث مدخلات المكون بشكل صحيح.
مع ظهور Angular Signals، يبدو أن مستقبل إدارة الحالة في Angular يتجه نحو تبني نموذج Reactivity أكثر دقة وفعالية. هذا لا يعني أن تقنيات إدارة الحالة الحالية مثل NgRx أو Services ستختفي، لكنها قد تصبح أقل أهمية مع مرور الوقت. في الواقع، بدأت بعض المكتبات مثل NgRx بالفعل في دمج دعم لـ Signals، مما يتيح لك الاستفادة من كلتا التقنيتين معاً.
من تجربتي الشخصية، وجدت أن Signals تُبسط بشكل كبير إدارة الحالة في التطبيقات المتوسطة والصغيرة. بدلاً من الحاجة إلى كتابة الكثير من الكود الإضافي لإدارة التغييرات والتحديثات، يمكنك الآن التركيز على منطق التطبيق نفسه. ومع ذلك، في التطبيقات الكبيرة والمعقدة، قد لا تزال بحاجة إلى حلول إدارة حالة أكثر قوة مثل NgRx، خاصة إذا كنت بحاجة إلى تتبع تاريخ التغييرات أو إدارة حالة مشتركة بين مكونات متعددة.
لكن الشيء المثير حقاً هو كيف ستتطور Signals في المستقبل. مع استمرار فريق Angular في تحسين هذه الميزة، يمكننا توقع ظهور أدوات ومكتبات جديدة مبنية على Signals، مما سيجعل إدارة الحالة أسهل وأكثر قوة. على سبيل المثال، قد نرى مكتبات جديدة لإدارة الحالة تعتمد بالكامل على Signals، أو أدوات لتصحيح الأخطاء تساعد على تتبع تدفق البيانات داخل التطبيق.
في النهاية، أن Angular Signals ليست مجرد ميزة عابرة، بل هي خطوة مهمة نحو جعل Angular أكثر حداثة وفعالية. إذا كنت تعمل على مشروع Angular جديد، فإنني أوصي بشدة بتجربة Signals منذ البداية. وإذا كنت تعمل على مشروع حالي، ففكر في كيفية دمج Signals تدريجياً لتحسين أداء التطبيق وتبسيط الكود. المستقبل يبدو مشرقاً لـ Angular، وSignals هي مجرد البداية.
إذا كنت تريد البدء باستخدام Angular Signals في مشروعك اليوم، فإليك بعض النصائح العملية التي ستساعدك على الاستفادة القصوى منها دون الوقوع في الفخاخ الشائعة. أولاً، ابدأ بتحويل المكونات الصغيرة التي تعتمد على حالة بسيطة إلى Signals. هذا سيساعدك على فهم كيفية عملها دون الحاجة إلى إعادة كتابة الكثير من الكود. على سبيل المثال، يمكنك تحويل مكون عداد بسيط أو قائمة صغيرة إلى Signals، ثم قياس الفرق في الأداء وسهولة الصيانة.
ثانياً، استخدم computed للبيانات المشتقة بدلاً من حسابها يدوياً في كل مرة. هذا لن يجعل الكود أكثر نظافة فحسب، بل سيحسن أيضاً من أداء التطبيق عن طريق تجنب الحسابات غير الضرورية. تذكر أن computed تُحدث قيمتها فقط عند قراءة قيمتها، وليس عند تغيير الـ Signals التي تعتمد عليها، مما يقلل من الحمل على المعالج.
ثالثاً، كن حذراً عند دمج Signals مع RxJS. على الرغم من أن الاثنين يعملان معاً بشكل جيد، إلا أن هناك بعض الفروق الدقيقة التي يجب الانتباه إليها. على سبيل المثال، إذا كنت تستخدم toSignal مع Observable، فتأكد من توفير قيمة أولية لتجنب الأخطاء عند قراءة الـ Signal قبل أن يُصدر الـ Observable قيمة. بالإضافة إلى ذلك، تذكر أن الـ Signals ليست بديلاً كاملاً لـ RxJS، بل هي أداة مكملة تُستخدم لإدارة الحالة المتزامنة داخل المكونات.
أخيراً، لا تحاول إعادة كتابة كل شيء باستخدام Signals دفعة واحدة. بدلاً من ذلك، قم بتطبيقها تدريجياً في أجزاء مختلفة من التطبيق، وقم بقياس تأثيرها على الأداء وسهولة الصيانة. تذكر أن الهدف ليس استخدام أحدث التقنيات لمجرد استخدامها، بل تحسين جودة الكود وقابلية الصيانة. إذا وجدت أن Signals تُبسط الكود وتحسن الأداء، فاستمر في استخدامها. وإذا واجهت مشاكل، فلا تتردد في العودة إلى الحلول التقليدية حتى تفهم تماماً كيفية عملها.
في النهاية، Angular Signals تمثل خطوة مهمة نحو جعل تطوير تطبيقات الويب أكثر فعالية وسهولة. سواء كنت مطوراً جديداً أو محترفاً ذو خبرة، فإن فهم كيفية عملها واستخدامها بشكل صحيح سيمنحك ميزة حقيقية في سوق العمل. ابدأ اليوم، وجربها في مشروعك التالي، وشارك تجربتك مع المجتمع. المستقبل هنا، وهو يبدو مشرقاً جداً.