تعرّف على Angular Signals من الداخل: كيف تعمل خلف الكواليس، متى تستخدمها بدلاً من RxJS، والأخطاء الشائعة التي يقع فيها حتى المطورون المحترفون. دليل عملي لفهم الثورة القادمة في إدارة الحالة داخل Angular.
قبل عامين، كنت أعمل على تطبيق مصرفي ضخم مبني بـ Angular. المشكلة؟ كل مرة نغير قيمة في الـ state، كان التطبيق يعيد رسم مكونات كاملة دون داعٍ، حتى تلك التي لا تعتمد على هذه القيمة. النتيجة؟ الـ UI يتجمد لثوانٍ، والمستخدمون يشكون من بطء غير مفهوم. حينها قلت لنفسي: «يبدو أن Angular بحاجة لشيء جديد». هذا الشيء كان Angular Signals، الذي ظهر في الإصدار 16 كخطوة أولى نحو تغيير جذري في كيفية إدارة الحالة داخل الإطار. لكن هل هي حقاً ثورة كما يدعي البعض، أم مجرد إضافة أخرى ستُنسى بعد عام؟
الحقيقة هي أن Angular Signals ليست مجرد ميزة جديدة، بل إعادة تفكير كاملة في كيفية تتبع التبعيات بين المكونات والبيانات. في عالم الـ JavaScript الحديث، حيث أصبحت التطبيقات أكثر تعقيداً من أي وقت مضى، أصبحت إدارة الحالة أمراً بالغ الأهمية. لكن المشكلة الأكبر ليست في كيفية تغيير الحالة، بل في كيفية معرفة متى يجب تحديث واجهة المستخدم بناءً على هذه التغييرات. هنا يأتي دور الـ Signals، التي تقدم نموذجاً جديداً يعتمد على مفهوم «التتبع الدقيق للتبعيات» بدلاً من الـ Change Detection التقليدي الذي يعتمد على الـ Zone.js.
لفهم Angular Signals، يجب أولاً فهم المشكلة التي تحاول حلها. في Angular التقليدي، يعتمد الإطار على Zone.js لالتقاط التغييرات في الحالة. الـ Zone.js هو مكتبة تقوم بـ monkey patching للعديد من وظائف الـ JavaScript الأصلية (مثل setTimeout، addEventListener، وغيرها) لتتبع متى تحدث تغييرات قد تؤثر على واجهة المستخدم. عندما يحدث تغيير، يقوم Angular بتشغيل دورة الـ Change Detection، التي تمر على جميع المكونات في الشجرة وتتحقق من التغييرات.
المشكلة هنا هي أن هذا النموذج «جشع» بطبيعته. حتى لو تغيرت قيمة واحدة صغيرة في جزء بعيد من التطبيق، قد ينتهي الأمر بإعادة رسم مكونات كاملة لا علاقة لها بهذه القيمة. هذا ليس فقط غير فعال من حيث الأداء، بل يجعل من الصعب التنبؤ بسلوك التطبيق، خاصة في التطبيقات الكبيرة والمعقدة. هنا تأتي Angular Signals بمفهوم جديد: «التتبع الدقيق للتبعيات». بدلاً من الاعتماد على Zone.js، تسمح الـ Signals لـ Angular بمعرفة بالضبط أي مكونات تعتمد على أي قيمة، وبالتالي تحديث فقط ما يحتاج إلى التحديث.
// مثال بسيط على Signal في Angular
import { signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `{{ count() }} <button (click)="increment()">زيادة</button>`
})
export class CounterComponent {
// تعريف Signal مع القيمة الأولية
count = signal(0);
increment() {
// تحديث قيمة Signal
this.count.update(current => current + 1);
}
}
// في القالب، نستخدم count() بدلاً من count مباشرة
// لأن الـ Signal هي دالة getterفي المثال أعلاه، عندما نستخدم `count()` في القالب، يخبر Angular تلقائياً أن هذا المكون يعتمد على الـ Signal المسمى `count`. عندما تتغير قيمة الـ Signal (من خلال `update` أو `set`)، يعرف Angular بالضبط أن هذا المكون فقط يحتاج إلى التحديث، وليس أي مكون آخر في التطبيق. هذا يختلف تماماً عن النموذج التقليدي، حيث كان Angular يعيد رسم جميع المكونات في الشجرة بدءاً من الجذر.
لفهم كيفية عمل Angular Signals، يجب الغوص قليلاً في التفاصيل التقنية لما يحدث خلف الكواليس. عندما تنشئ Signal باستخدام `signal(0)`، فإنك في الواقع تنشئ كائناً يحتوي على عدة خصائص داخلية. أهم هذه الخصائص هي:
عندما تقرأ قيمة الـ Signal باستخدام `count()`، يحدث أمران مهمان خلف الكواليس:
عندما تتغير قيمة الـ Signal (من خلال `set` أو `update`)، يحدث ما يلي:
هذا النموذج يختلف تماماً عن الـ Change Detection التقليدي، الذي كان يعتمد على مقارنة القيم الفعلية باستخدام `===`. في النموذج الجديد، لا يحتاج Angular إلى مقارنة القيم على الإطلاق، بل يعتمد فقط على رقم الإصدار، مما يجعل العملية أسرع بكثير وأكثر كفاءة.
// مثال على computed Signal مع تتبع التبعيات
import { signal, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<p>الاسم: {{ fullName() }}</p>
<p>العمر: {{ age() }}</p>
<p>العمر بعد 5 سنوات: {{ ageInFiveYears() }}</p>
`
})
export class UserProfileComponent {
firstName = signal('أحمد');
lastName = signal('محمد');
age = signal(30);
// computed Signal يعتمد على إشارات أخرى
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
ageInFiveYears = computed(() => this.age() + 5);
updateProfile() {
this.firstName.set('خالد');
this.age.update(a => a + 1);
// فقط fullName و ageInFiveYears سيتأثران بالتغييرات، وليس المكون بأكمله
}
}في المثال أعلاه، عندما تتغير قيمة `firstName` أو `age`، يعرف Angular بالضبط أن `fullName` و `ageInFiveYears` فقط يحتاجان إلى إعادة الحساب، وليس أي جزء آخر من القالب. هذا النوع من التتبع الدقيق للتبعيات هو ما يجعل Angular Signals قوية للغاية، خاصة في التطبيقات الكبيرة حيث كانت إعادة رسم المكونات الكاملة تسبب مشاكل في الأداء.
هذا هو السؤال الذي يشغل بال الكثير من المطورين حالياً. هل يجب علينا التخلي عن RxJS بالكامل والانتقال إلى Signals؟ الإجابة القصيرة هي: لا. RxJS و Signals هما أداتان مختلفتان لحل مشاكل مختلفة، وكل منهما له مكانه في التطبيقات الحديثة. لفهم متى يجب استخدام كل منهما، دعونا نقارن بين النموذجين:
في رأيي الشخصي، أفضل نهج هو استخدام كل أداة في مكانها المناسب. على سبيل المثال، يمكنك استخدام RxJS للتعامل مع الـ API calls، ثم تحويل الـ Observables إلى Signals باستخدام `toSignal` لعرض البيانات في واجهة المستخدم. هذا يجمع بين قوة RxJS في التعامل مع البيانات غير المتزامنة وسهولة الـ Signals في إدارة الحالة داخل المكونات.
// دمج RxJS مع Signals باستخدام toSignal
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { signal } from '@angular/core';
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="users() as users">
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</div>
<div *ngIf="loading()">جاري التحميل...</div>
<div *ngIf="error() as error">{{ error }}</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Signal للتحكم في حالة التحميل
loading = signal(true);
// تحويل Observable إلى Signal
users = toSignal(this.http.get<User[]>('https://api.example.com/users'), {
initialValue: [],
});
// Signal لعرض الأخطاء
error = signal<string | null>(null);
ngOnInit() {
this.http.get<User[]>('https://api.example.com/users').subscribe({
next: (users) => {
this.users.set(users); // تحديث Signal بالقيم الجديدة
this.loading.set(false);
},
error: (err) => {
this.error.set('فشل تحميل البيانات');
this.loading.set(false);
}
});
}
}
interface User {
id: number;
name: string;
}في هذا المثال، نستخدم RxJS للتعامل مع الـ HTTP request، ثم نحول الـ Observable إلى Signal باستخدام `toSignal`. هذا يسمح لنا بالاستفادة من قوة RxJS في التعامل مع البيانات غير المتزامنة، وفي نفس الوقت نستفيد من سهولة الـ Signals في إدارة الحالة داخل المكونات. لاحظ كيف استخدمنا `loading` و `error` كإشارات مستقلة للتحكم في حالة واجهة المستخدم، مما يجعل الكود أكثر وضوحاً وسهولة في الصيانة.
على الرغم من أن Angular Signals تبدو بسيطة في الاستخدام، إلا أن هناك العديد من الفخاخ التي يمكن أن يقع فيها حتى المطورون المحترفون. إليك بعض الأخطاء الشائعة وكيفية تجنبها:
أحد الأخطاء الشائعة هو محاولة استخدام Signals في كل مكان، حتى في الحالات التي لا تكون فيها ضرورية. على سبيل المثال، إذا كان لديك مكون بسيط جداً لا يعتمد على أي حالة خارجية، فقد لا يكون هناك داعٍ لاستخدام Signals على الإطلاق. في هذه الحالات، قد يكون استخدام المتغيرات العادية أكثر كفاءة وأبسط في الفهم. تذكر أن الـ Signals تأتي مع بعض الـ Overhead، لذلك يجب استخدامها فقط عندما توفر قيمة حقيقية في الأداء أو الوضوح.
هذا خطأ شائع جداً بين المطورين الجدد في استخدام Signals. عندما تريد قراءة قيمة Signal، يجب عليك استخدام `signalName()` بدلاً من `signalName` مباشرة. إذا نسيت الأقواس، فستحصل على الكائن Signal نفسه بدلاً من قيمته، مما قد يؤدي إلى أخطاء غريبة في واجهة المستخدم. على سبيل المثال:
// ❌ خطأ: نسيان الأقواس
<p>{{ count }}</p> // سيظهر [object Object] بدلاً من القيمة
// ✅ صحيح: استخدام الأقواس
<p>{{ count() }}</p> // سيظهر القيمة الفعليةإذا قمت بتعديل قيمة Signal خارج سياق Zone.js (على سبيل المثال، داخل callback من مكتبة خارجية أو داخل Web Worker)، فقد لا يتم تحديث واجهة المستخدم كما هو متوقع. هذا لأن Angular يعتمد على Zone.js لالتقاط التغييرات في الحالة. لحل هذه المشكلة، يمكنك استخدام `NgZone.run` لتشغيل الكود داخل Zone.js:
import { NgZone } from '@angular/core';
constructor(private ngZone: NgZone) {}
someExternalCallback(newValue: number) {
this.ngZone.run(() => {
this.count.set(newValue); // سيتم تحديث واجهة المستخدم بشكل صحيح
});
}من الأخطاء الخطيرة التي يمكن أن تقع فيها هو إنشاء دوائر تبعية في الـ computed Signals. على سبيل المثال، إذا كان لديك computed Signal يعتمد على نفسه بطريقة غير مباشرة، فقد ينتهي بك الأمر بدائرة لا نهائية تؤدي إلى تجميد التطبيق. Angular ستحاول اكتشاف هذه الدوائر وإلقاء خطأ، لكن من الأفضل تجنبها تماماً من البداية. إليك مثال على دائرة تبعية خطيرة:
// ❌ دائرة تبعية خطيرة
const a = signal(1);
const b = computed(() => a() + 1);
const c = computed(() => b() + 1);
// هذا سيؤدي إلى خطأ
const d = computed(() => c() + a()); // a يعتمد على c الذي يعتمد على b الذي يعتمد على aمنذ ظهور Angular Signals في الإصدار 16، أصبح واضحاً أن فريق Angular يراهن بشكل كبير على هذه الميزة كمستقبل لإدارة الحالة داخل الإطار. في الإصدار 17، رأينا المزيد من التكامل بين Signals والنظام البيئي لـ Angular، مثل دعم الـ Signals في الـ Router و الـ Forms. لكن هذا مجرد بداية، وهناك العديد من الميزات المثيرة التي نتوقع رؤيتها في الإصدارات القادمة.
أحد أكثر الأمور إثارة هو التكامل الأعمق بين Signals و RxJS. حالياً، يمكنك تحويل الـ Observables إلى Signals باستخدام `toSignal`، لكننا نتوقع رؤية المزيد من الأدوات التي تجعل هذا التكامل أكثر سلاسة. على سبيل المثال، قد نرى دوال جديدة مثل `toObservable` لتحويل الـ Signals إلى Observables بسهولة، مما يسهل دمج الـ Signals مع المكتبات التي تعتمد على RxJS.
هناك أيضاً حديث عن دعم الـ Signals في الـ Angular Universal (الـ Server-Side Rendering). حالياً، الـ Signals تعمل بشكل جيد في بيئة المتصفح، لكن هناك بعض التحديات في بيئة الخادم، خاصة فيما يتعلق بالتزامن والحالة المشتركة بين الطلبات. نتوقع أن يرى فريق Angular حلولاً لهذه التحديات في الإصدارات القادمة، مما يجعل من الممكن استخدام الـ Signals في تطبيقات الـ SSR دون مشاكل.
أخيراً، هناك توقعات بأن نرى المزيد من الأدوات المساعدة لـ Signals في مكتبة Angular نفسها. على سبيل المثال، قد نرى دوال جديدة مثل `effect` لإدارة الآثار الجانبية بطريقة أكثر كفاءة، أو أدوات لتحسين أداء الـ computed Signals في الحالات المعقدة. مهما كانت الميزات الجديدة، من الواضح أن Angular Signals هنا لتبقى، وأنها ستشكل مستقبل إدارة الحالة في Angular لسنوات قادمة.
بعد أكثر من عام من العمل مع Angular Signals في مشاريع حقيقية، يمكنني القول بثقة: إذا كنت تعمل على تطبيق متوسط أو كبير الحجم، فابدأ في استخدام Signals اليوم. لا تنتظر حتى تصبح جزءاً من «الماضي». ابدأ بمكونات صغيرة، ثم قم بتوسيع استخدامها تدريجياً. لكن تذكر، لا تستخدم Signals في كل مكان فقط لأنها جديدة. استخدمها حيث توفر قيمة حقيقية في الأداء أو الوضوح، واترك RxJS للتعامل مع البيانات غير المتزامنة والمعقدة.
الخطوة العملية التالية؟ اختر مكوناً واحداً في تطبيقك الحالي، وحاول إعادة كتابته باستخدام Signals بدلاً من الـ Change Detection التقليدي. سترى الفرق بنفسك في الأداء والسلاسة. وعندما تفعل ذلك، لا تنسَ مراقبة الـ Memory Usage و الـ Change Detection Cycles باستخدام أدوات المطور في المتصفح. هذا هو أفضل طريقة لفهم كيف تغير Signals قواعد اللعبة حقاً.