Angular Signals ليست مجرد ميزة جديدة، إنها إعادة تفكير جذرية في كيفية إدارة الحالة في تطبيقات الويب الحديثة. اكتشف كيف تعمل تحت الغطاء، متى تستخدمها، ومتى تبتعد عنها، مع أمثلة عملية من مشاريع حقيقية.
في أحد المشاريع الكبيرة الذي عملت عليه العام الماضي، واجهنا مشكلة غريبة: التطبيق كان يعمل ببطء شديد عند تحديث البيانات، رغم أننا كنا نستخدم ChangeDetection.OnPush ونحاول تقليل إعادة الرسم قدر الإمكان. المشكلة؟ كنا نعتمد على RxJS بشكل مفرط، وكان لدينا عشرات الـ Subjects و BehaviorSubjects التي تتفاعل مع بعضها البعض بطريقة لا يمكن تتبعها. عندما جربنا Angular Signals لأول مرة، انخفض وقت التحديث من ٤٥٠ مللي ثانية إلى ٨٠ مللي ثانية فقط. ليس هذا فحسب، بل أصبح الكود أسهل في القراءة والصيانة. هذا ليس مجرد تحسين بسيط، بل هو تغيير في طريقة تفكيرنا في إدارة الحالة.
Angular Signals ليست مجرد إضافة جديدة لإطار العمل، بل هي تحول جذري في كيفية تعامل Angular مع التحديثات والتفاعلات. في هذا المقال، لن نتحدث عن ماهية Signals بشكل سطحي، بل سنغوص في تفاصيل كيفية عملها تحت الغطاء، متى يجب استخدامها، ومتى يجب تجنبها. سنناقش أيضاً كيف تتفاعل مع RxJS، وكيف يمكن استخدامها لتحسين أداء التطبيقات الكبيرة دون الحاجة لإعادة كتابة كل شيء من الصفر.
لفهم Angular Signals، يجب أولاً أن نفهم المشكلة التي تحاول حلها. في التطبيقات التقليدية، تعتمد Angular على نظام كشف التغييرات (Change Detection) الذي يعمل بشكل تسلسلي، حيث يقوم بفحص كل مكون في الشجرة بدءاً من الجذر حتى الأوراق. هذا النظام فعال ولكنه يمكن أن يكون بطيئاً في التطبيقات الكبيرة، خاصة عندما يكون لدينا مكونات كثيرة تعتمد على البيانات نفسها. على سبيل المثال، إذا كان لدينا مكون رئيسي يحتوي على ٥٠ مكون فرعي، وكلهم يعتمدون على نفس البيانات، فإن أي تغيير في هذه البيانات سيؤدي إلى إعادة رسم كل المكونات، حتى لو كان التغيير بسيطاً مثل تحديث قيمة واحدة في كائن كبير.
Angular Signals تأتي لحل هذه المشكلة من خلال نظام ردود الفعل الدقيقة (Fine-Grained Reactivity). بدلاً من إعادة رسم كل المكونات عند تغيير البيانات، تقوم Signals بتتبع الاعتماديات بين المكونات والبيانات بشكل دقيق، بحيث يتم تحديث المكونات التي تعتمد على البيانات المتغيرة فقط. هذا يشبه إلى حد كبير كيفية عمل React مع الـ Hooks، ولكن مع فرق كبير في التنفيذ والتكامل مع بقية نظام Angular. تحت الغطاء، تعتمد Signals على مفهوم يسمى "الاعتماديات التفاعلية"، حيث يتم تتبع كل قراءة لقيمة Signal داخل نطاق معين، ويتم تسجيل هذه القراءة كاعتمادية. عندما تتغير قيمة Signal، يتم إخطار جميع الاعتماديات المسجلة فقط، دون الحاجة إلى فحص كامل الشجرة.
// مثال بسيط يوضح كيفية تعريف واستخدام Signal
import { signal } from '@angular/core';
// تعريف Signal
const count = signal(0);
// قراءة القيمة
console.log(count()); // 0
// تحديث القيمة
count.set(5);
console.log(count()); // 5
// تحديث القيمة بناءً على القيمة الحالية
count.update(value => value + 1);
console.log(count()); // 6
// استخدام mutate لتعديل كائنات معقدة
const user = signal({ name: 'Ahmed', age: 30 });
user.mutate(current => {
current.age += 1;
});
console.log(user().age); // 31لفهم كيفية عمل Signals بشكل عميق، يجب أن نلقي نظرة على ما يحدث داخل محرك JavaScript والمعالج. عندما تقوم بتعريف Signal باستخدام الدالة signal()، فإن Angular يقوم بإنشاء كائن خاص يحتوي على عدة خصائص مهمة: القيمة الحالية، قائمة الاعتماديات، ودالة التحديث. هذا الكائن يتم تخزينه في الذاكرة كمثيل من فئة داخلية تسمى SignalNode. عندما تقوم بقراءة قيمة Signal باستخدام الدالة getter (التي يتم استدعاؤها عند كتابة signal())، يقوم Angular بتسجيل هذه القراءة كاعتمادية في السياق الحالي. هذا التسجيل يتم باستخدام مفهوم يسمى "التتبع النشط" (Active Tracking)، حيث يتم استخدام متغير عام داخل Angular لتتبع السياق الحالي.
عندما تقوم بتحديث قيمة Signal باستخدام set() أو update()، يقوم Angular بإجراء عدة خطوات تحت الغطاء: أولاً، يتم التحقق مما إذا كانت القيمة الجديدة مختلفة عن القيمة الحالية باستخدام خوارزمية مقارنة متقدمة (ليست مجرد مقارنة بسيطة باستخدام ===). إذا كانت القيمة مختلفة، يتم تحديث القيمة الداخلية، ثم يتم إخطار جميع الاعتماديات المسجلة. هذا الإخطار لا يحدث بشكل فوري، بل يتم إضافته إلى قائمة المهام (Task Queue) داخل الـ Event Loop، مما يضمن أن التحديثات تحدث بشكل غير متزامن وتجنب أي مشاكل في الأداء بسبب التحديثات المتكررة. هذا يعني أن إذا قمت بتحديث Signal عدة مرات في نفس دورة الـ Event Loop، فإن المكونات التي تعتمد على هذه Signal لن يتم إعادة رسمها إلا مرة واحدة فقط، مما يحسن الأداء بشكل كبير.
// مثال يوضح كيفية تتبع الاعتماديات تحت الغطاء
import { signal, effect } from '@angular/core';
const price = signal(100);
const quantity = signal(2);
const tax = signal(0.1);
// تعريف effect لتتبع الاعتماديات
const totalEffect = effect(() => {
const total = price() * quantity() * (1 + tax());
console.log(`Total: ${total}`);
// تحت الغطاء، يقوم Angular بتسجيل price, quantity, tax كاعتماديات لهذا effect
});
// تحديث قيمة price
price.set(150); // سيؤدي إلى إعادة حساب totalEffect
// تحديث قيمة quantity
quantity.set(3); // سيؤدي أيضاً إلى إعادة حساب totalEffect
// تحديث قيمة tax
tax.set(0.15); // سيؤدي إلى إعادة حساب totalEffect
// إذا قمت بتحديث price مرتين في نفس دورة الـ Event Loop
price.set(200);
price.set(250); // سيتم إعادة حساب totalEffect مرة واحدة فقطعلى الرغم من أن Signals تبدو بسيطة وسهلة الاستخدام، إلا أنها تحمل بعض الفخاخ التي يمكن أن تؤدي إلى مشاكل في الأداء أو حتى Memory Leaks إذا لم يتم استخدامها بحذر. أحد هذه الفخاخ هو استخدام Signals داخل الـ effects أو الـ computed values بدون فهم كيفية عمل الـ Cleanup. على سبيل المثال، إذا قمت بتعريف effect يتفاعل مع Signal، ثم قمت بتدمير المكون الذي يحتوي على هذا effect، فإن Angular لن يقوم بإلغاء تسجيل الاعتماديات تلقائياً. هذا يمكن أن يؤدي إلى تسرب في الذاكرة، خاصة في التطبيقات الكبيرة التي تحتوي على مكونات ديناميكية كثيرة.
مشكلة أخرى شائعة هي استخدام Signals داخل حلقات تكرار (loops) بدون فهم كيفية تأثير ذلك على الأداء. على سبيل المثال، إذا كان لديك مصفوفة تحتوي على ١٠٠٠ عنصر، وقمت بتعريف Signal لكل عنصر داخل حلقة تكرار، فإن أي تغيير في هذه المصفوفة سيؤدي إلى إعادة حساب جميع الـ computed values التي تعتمد على هذه الإشارات، مما قد يؤدي إلى بطء في الأداء. الحل هو استخدام تقنيات مثل memoization أو تقسيم البيانات إلى أجزاء أصغر، أو حتى استخدام RxJS في بعض الحالات بدلاً من Signals.
// مثال يوضح كيفية تجنب Memory Leak في effects
import { signal, effect, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-example',
template: `...`,
})
export class ExampleComponent {
private destroyRef = inject(DestroyRef);
private price = signal(100);
private quantity = signal(2);
constructor() {
// استخدام takeUntilDestroyed لتجنب Memory Leak
effect(() => {
const total = this.price() * this.quantity();
console.log(`Total: ${total}`);
}, { injector: this.destroyRef.injector });
// أو باستخدام DestroyRef مباشرة
const cleanupEffect = effect(() => {
const total = this.price() * this.quantity();
console.log(`Total: ${total}`);
});
this.destroyRef.onDestroy(() => cleanupEffect.destroy());
}
}القرار باستخدام Angular Signals أو عدم استخدامها ليس قراراً بسيطاً، بل يعتمد على عدة عوامل تتعلق بطبيعة المشروع وحجمه وتعقيده. في رأيي، Signals هي الخيار الأمثل في الحالات التالية: عندما يكون لديك حالة محلية داخل مكون واحد وتحتاج إلى تحديثها بشكل متكرر، أو عندما يكون لديك مكونات كثيرة تعتمد على نفس البيانات وتحتاج إلى تحسين الأداء. على سبيل المثال، في أحد المشاريع الذي عملت عليه، كان لدينا لوحة تحكم تحتوي على أكثر من ٢٠ مكوناً فرعياً، وكلهم يعتمدون على نفس البيانات من واجهة برمجة التطبيقات. باستخدام Signals، تمكنا من تقليل وقت التحديث من ٦٠٠ مللي ثانية إلى ١٢٠ مللي ثانية فقط، مما حسن تجربة المستخدم بشكل كبير.
من ناحية أخرى، هناك حالات يجب فيها تجنب استخدام Signals والاعتماد على RxJS بدلاً منها. على سبيل المثال، إذا كان لديك تدفق بيانات معقد يحتوي على عمليات تحويل متعددة (مثل map, filter, merge)، أو إذا كنت بحاجة إلى التعامل مع أحداث متكررة مثل أحداث الفأرة أو لوحة المفاتيح، فإن RxJS يظل الخيار الأفضل. أيضاً، إذا كان لديك حالة عامة تحتاج إلى مشاركتها بين مكونات بعيدة في الشجرة، فإن استخدام خدمة مع RxJS قد يكون أكثر فعالية من استخدام Signals. في النهاية، القرار يعتمد على فهم عميق للمشكلة التي تحاول حلها والأدوات المتاحة لديك.
واحدة من الأسئلة التي يطرحها المطورون كثيراً هي: هل يجب استخدام Angular Signals بدلاً من RxJS، أم يمكن استخدام كلاهما معاً؟ الحقيقة هي أن Signals و RxJS ليسا عدوين، بل يمكن أن يكونا حليفين قويين إذا تم استخدامهما بشكل صحيح. في الواقع، Angular توفر أدوات خاصة لتحويل Signals إلى Observables والعكس، مما يسمح لك بالاستفادة من مزايا كل منهما. على سبيل المثال، يمكنك استخدام toSignal لتحويل Observable إلى Signal، مما يسمح لك باستخدام البيانات من واجهة برمجة التطبيقات داخل مكونات تعتمد على Signals. وبالمثل، يمكنك استخدام toObservable لتحويل Signal إلى Observable، مما يسمح لك باستخدام تدفقات البيانات المعقدة مع RxJS.
في أحد المشاريع الذي عملت عليه، كنا نستخدم RxJS بشكل مكثف للتعامل مع البيانات من واجهة برمجة التطبيقات، ولكن واجهنا مشكلة في الأداء عند تحديث المكونات. الحل كان استخدام toSignal لتحويل الـ Observables إلى Signals داخل المكونات، مما سمح لنا بالاستفادة من نظام ردود الفعل الدقيقة في Signals دون الحاجة إلى إعادة كتابة كل الكود. هذا التكامل بين Signals و RxJS يفتح الباب أمام إمكانيات جديدة في إدارة الحالة، حيث يمكنك استخدام RxJS للتعامل مع تدفقات البيانات المعقدة واستخدام Signals لإدارة الحالة المحلية داخل المكونات.
// مثال يوضح التكامل بين Signals و RxJS
import { signal, toSignal, toObservable } from '@angular/core';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-integration',
template: `...`,
})
export class IntegrationComponent {
// تحويل Observable إلى Signal
private timer$ = interval(1000).pipe(
map(value => `Timer: ${value}`)
);
timerSignal = toSignal(this.timer$, { initialValue: 'Timer: 0' });
// تحويل Signal إلى Observable
private count = signal(0);
count$ = toObservable(this.count);
constructor() {
// استخدام Signal داخل Observable
this.count$.subscribe(value => {
console.log(`Count changed to: ${value}`);
});
}
increment() {
this.count.update(value => value + 1);
}
}بعد أن تتقن الأساسيات، ستجد أن Signals تقدم إمكانيات متقدمة يمكن أن تساعدك في حل مشاكل معقدة في إدارة الحالة. أحد هذه السيناريوهات هو استخدام Signals مع الـ Dynamic Components. في التطبيقات الكبيرة، غالباً ما نحتاج إلى تحميل المكونات ديناميكياً بناءً على البيانات أو تفضيلات المستخدم. باستخدام Signals، يمكنك إدارة حالة هذه المكونات بسهولة، وتحديثها بناءً على التغييرات في البيانات دون الحاجة إلى إعادة تحميل المكون بالكامل. على سبيل المثال، في أحد المشاريع، كنا نستخدم Signals لإدارة حالة مكونات الجداول الديناميكية، حيث كان كل صف في الجدول مكوناً مستقلاً يتم تحميله ديناميكياً. باستخدام Signals، تمكنا من تحديث حالة كل صف بشكل مستقل دون التأثير على بقية الصفوف، مما حسن الأداء بشكل كبير.
سيناريو آخر متقدم هو استخدام Signals مع الـ State Management Libraries مثل NgRx. على الرغم من أن NgRx يعتمد على RxJS بشكل أساسي، إلا أنه يمكن دمجه مع Signals لتحسين الأداء في بعض الحالات. على سبيل المثال، يمكنك استخدام Signals لإدارة الحالة المحلية داخل المكونات، واستخدام NgRx لإدارة الحالة العامة للتطبيق. هذا الدمج يمكن أن يكون مفيداً في التطبيقات الكبيرة التي تحتوي على حالة عامة معقدة وحالة محلية بسيطة. في أحد المشاريع الذي عملت عليه، استخدمنا هذا النهج لتقليل حجم الـ Store في NgRx، مما حسن الأداء وجعل الكود أسهل في الصيانة.
// مثال يوضح استخدام Signals مع Dynamic Components
import { signal, ComponentRef, ViewContainerRef } from '@angular/core';
import { DynamicComponent } from './dynamic.component';
@Component({
selector: 'app-dynamic-host',
template: `<ng-container #container></ng-container>`,
})
export class DynamicHostComponent {
private componentRefs: ComponentRef<DynamicComponent>[] = [];
private data = signal<{ id: number, name: string }[]>([]);
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
constructor() {
// تحديث البيانات ديناميكياً
this.data.set([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]);
this.renderComponents();
}
renderComponents() {
this.container.clear();
this.comp [];
this.data().forEach(item => {
const componentRef = this.container.createComponent(DynamicComponent);
componentRef.instance.data = item;
this.componentRefs.push(componentRef);
});
}
addItem() {
this.data.update(items => [...items, { id: items.length + 1, name: `Item ${items.length + 1}` }]);
this.renderComponents();
}
}بعد أكثر من عام من العمل مع Angular Signals في مشاريع حقيقية، هذه هي نصائحي الصريحة لك: لا تستخدم Signals لكل شيء فقط لأنها جديدة، بل استخدمها حيث تحقق قيمة حقيقية. ابدأ بإدارة الحالة المحلية داخل المكونات، ثم انتقل إلى الحالات الأكثر تعقيداً فقط عندما تكون واثقاً من فهمك لكيفية عملها تحت الغطاء. دائماً قم بقياس الأداء قبل وبعد استخدام Signals، لأن الفوائد قد لا تكون واضحة في التطبيقات الصغيرة. تذكر أن Signals ليست بديلاً كاملاً لـ RxJS، بل هي أداة مكملة. استخدم toSignal و toObservable بحكمة لتجنب تحويلات غير ضرورية قد تؤثر على الأداء.
وأخيراً، لا تخف من التجربة. قم بإنشاء مشروع صغير وجرب مختلف السيناريوهات مع Signals، مثل استخدامها مع الـ Dynamic Components أو دمجها مع NgRx. كلما فهمت أكثر كيف تعمل تحت الغطاء، كلما أصبحت أفضل في اتخاذ القرارات الصحيحة بشأن متى وكيف تستخدمها. Angular Signals ليست مجرد ميزة جديدة، بل هي خطوة نحو مستقبل إدارة الحالة في تطبيقات الويب، ومن المهم أن تكون مستعداً لهذا المستقبل.