Angular Signals ليست مجرد ميزة جديدة، بل ثورة في إدارة الحالة داخل التطبيقات. اكتشف كيف تعمل تحت الغطاء، متى تستخدمها بدلاً من RxJS، والأخطاء الشائعة التي يقع فيها المطورون عند الانتقال.
في آخر تحديث لـ Angular، ظهرت Signals كأداة جديدة لإدارة الحالة، لكنها ليست مجرد بديل لـ RxJS كما يعتقد الكثيرون. الحقيقة هي أن Signals تعيد تعريف كيفية تفاعل المكونات مع البيانات داخل التطبيقات الكبيرة، خاصةً عندما يتعلق الأمر بالأداء والتزامن. تخيل أنك تعمل على لوحة تحكم معقدة تحتوي على عشرات المكونات التي تعتمد على نفس الحالة، وكل تغيير صغير يؤدي إلى إعادة رسم كامل الواجهة. هنا تكمن قوة Signals: فهي تسمح بتحديثات دقيقة ومستهدفة دون الحاجة إلى إعادة بناء شجرة DOM بالكامل. لكن كيف يحدث هذا بالضبط؟ ولماذا يقول بعض المطورين إنها قد تجعل RxJS زائدة عن الحاجة في بعض الحالات؟
المشكلة الأساسية التي تعالجها Signals ليست جديدة: إدارة الحالة المتغيرة بكفاءة داخل التطبيقات أحادية الصفحة. في السابق، كنا نعتمد على RxJS لإدارة التدفقات التفاعلية، لكن هذا يأتي بتكلفة: منحنى تعلم حاد، تعقيد في إدارة الاشتراكات، ومشاكل في تتبع التبعيات بين المكونات. Signals تقدم نموذجاً أبسط وأكثر شفافية، حيث يتم تتبع التبعيات تلقائياً دون الحاجة إلى كتابة أكواد معقدة للاشتراكات والإلغاء. لكن هذا لا يعني أنها حل سحري لكل المشاكل. في هذا المقال، سنفكك كيفية عمل Signals تحت الغطاء، ونقارن بينها وبين RxJS في سيناريوهات واقعية، ونستكشف الأخطاء الشائعة التي يقع فيها المطورون عند استخدامها.
عندما تعلن عن signal باستخدام `signal()` في Angular، فإنك في الواقع تنشئ كائناً خاصاً يتتبع جميع القراءات والكتابات التي تحدث عليه. هذا الكائن لا يخزن القيمة فقط، بل يحتفظ أيضاً بقائمة بجميع التأثيرات (effects) والمكونات التي تعتمد على هذه القيمة. عندما تتغير القيمة، يتم تحديث جميع هذه التبعيات تلقائياً دون الحاجة إلى إعادة تنفيذ الكود بالكامل. هذا يختلف تماماً عن نموذج RxJS، حيث تحتاج إلى إدارة الاشتراكات يدوياً وتحديد متى يجب تحديث المكونات.
لفهم كيف يحدث هذا، دعنا نلقي نظرة على ما يحدث في الذاكرة. عندما تنشئ signal، يقوم Angular بإنشاء كائن من نوع `Signal` يحتوي على ثلاث خصائص رئيسية: `_value` لتخزين القيمة الحالية، `_equalityFn` لتحديد ما إذا كانت القيمة الجديدة مختلفة عن القديمة، و`_sources` لتتبع جميع المصادر التي تعتمد على هذا الـ signal. عندما تقرأ القيمة باستخدام `someSignal()`، يقوم Angular بتسجيل هذا القراءة في قائمة التبعيات الخاصة بالمكون الحالي. وعندما تتغير القيمة باستخدام `someSignal.set(newValue)`، يتم مقارنة القيمة الجديدة مع القديمة باستخدام `_equalityFn`، وإذا كانت مختلفة، يتم تحديث `_value` وإعلام جميع التبعيات المسجلة.
// مثال عملي يوضح كيفية تتبع التبعيات داخل Signals
import { signal, effect, computed } from '@angular/core';
// إنشاء signal أساسي
const counter = signal(0);
// computed signal يعتمد على counter
const doubleCounter = computed(() => counter() * 2);
// effect يتفاعل مع التغييرات في counter
const disposeEffect = effect(() => {
console.log(`Counter changed to: ${counter()}, Double: ${doubleCounter()}`);
});
// تغيير القيمة
counter.set(5); // سيطبع: Counter changed to: 5, Double: 10
counter.set(5); // لن يطبع شيء لأن القيمة لم تتغير
counter.update(v => v + 1); // سيطبع: Counter changed to: 6, Double: 12
// إلغاء التأثير
// disposeEffect();
// ملاحظات هامة:
// 1. computed signals تُحدث تلقائياً عند تغيير أي من تبعياتها
// 2. effects تعمل بشكل متزامن مع التغييرات، لكنها لا تمنع تحديث الواجهة
// 3. استخدام update يسمح بتعديل القيمة بناءً على القيمة الحالية دون الحاجة إلى قراءتها أولاًالعديد من المطورين يتساءلون عما إذا كانت Signals ستحل محل RxJS بالكامل. الإجابة القصيرة هي: لا، لكنهما يكملان بعضهما البعض. Signals مصممة لإدارة الحالة المتزامنة داخل التطبيق، بينما RxJS لا تزال الأداة الأفضل للتعامل مع التدفقات غير المتزامنة المعقدة مثل طلبات HTTP، WebSockets، أو أحداث المستخدم التي تحتاج إلى معالجة متقدمة مثل debounce أو throttle.
في تجربتي مع أحد المشاريع الكبيرة في شركة تكنولوجيا مالية، استخدمنا Signals لإدارة حالة المستخدم داخل لوحة التحكم، بينما احتفظنا بـ RxJS للتعامل مع تدفقات البيانات من واجهة برمجة التطبيقات الخارجية. هذا الفصل بين الأدوات سمح لنا بتحسين أداء التطبيق بشكل كبير، حيث قللت Signals من عدد إعادة رسم المكونات، بينما وفرت RxJS المرونة اللازمة للتعامل مع البيانات المتدفقة من السيرفر. إليك مقارنة سريعة بين الاستخدامين:
// مثال على الدمج بين Signals و RxJS
import { signal, toSignal, toObservable } from '@angular/core';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
// تحويل Observable إلى signal
const timer$ = interval(1000).pipe(map(v => `Tick: ${v}`));
const timerSignal = toSignal(timer$, { initialValue: 'Waiting...' });
// تحويل signal إلى Observable
const count = signal(0);
const count$ = toObservable(count);
// استخدامهما معاً
count$.subscribe(v => console.log(`Count changed to: ${v}`));
// في القالب:
// <p>{{ timerSignal() }}</p>
// <button (click)="count.update(v => v + 1)">Increment</button>
// ملاحظات هامة:
// 1. toSignal يحول Observable إلى signal مع قيمة ابتدائية
// 2. toObservable يحول signal إلى Observable بدون الحاجة إلى إدارة الاشتراكات يدوياً
// 3. هذا الدمج مفيد عند العمل مع مكتبات خارجية تعتمد على RxJSعندما بدأت باستخدام Signals في مشاريع حقيقية، وقعت في عدة فخاخ لم تذكرها معظم الدروس التعليمية. أحد أكبر الأخطاء التي ارتكبتها كان استخدام Signals داخل loops دون فهم كيفية تتبع التبعيات. على سبيل المثال، عند محاولة إنشاء مصفوفة من Signals داخل حلقة `for`، وجدت أن جميع المكونات كانت تعتمد على آخر signal تم إنشاؤه فقط، وليس على جميعها. هذا يحدث لأن Angular يتتبع التبعيات بناءً على سياق التنفيذ، وليس بناءً على الكائن نفسه.
مشكلة أخرى شائعة هي استخدام Signals مع الكائنات المعقدة دون تحديد دالة المساواة المناسبة. بشكل افتراضي، تستخدم Signals مقارنة مرجعية (`===`) لتحديد ما إذا كانت القيمة قد تغيرت. هذا يعني أنه إذا قمت بتعديل كائن موجود دون إنشاء مرجع جديد، فلن يتم تحديث المكونات المعتمدة على هذا الـ signal. الحل هو إما استخدام `signal()` مع دالة مساواة مخصصة، أو التأكد من إنشاء مرجع جديد عند التعديل.
// الأخطاء الشائعة وكيفية تجنبها
import { signal, effect } from '@angular/core';
// ❌ خطأ: استخدام نفس Signal داخل حلقة
const items = Array(5).fill(0).map((_, i) => signal(i));
// جميع المكونات ستعتمد على آخر signal فقط
// ✅ صحيح: استخدام مصفوفة من القيم مع signal واحد
const itemsSignal = signal(Array(5).fill(0));
// تحديث عنصر محدد
function updateItem(index: number, value: number) {
itemsSignal.update(arr => {
const newArr = [...arr];
newArr[index] = value;
return newArr;
});
}
// ❌ خطأ: تعديل كائن موجود دون تغيير المرجع
const user = signal({ name: 'Ahmed', age: 30 });
user().age = 31; // لن يحدث تحديث
// ✅ صحيح: إنشاء مرجع جديد
user.set({ ...user(), age: 31 });
// أو استخدام دالة مساواة مخصصة
const userWithEquality = signal(
{ name: 'Ahmed', age: 30 },
{ equal: (a, b) => a.age === b.age && a.name === b.name }
);
// ❌ خطأ: استخدام effects لتحديث الحالة
const count = signal(0);
effect(() => {
if (count() > 5) {
count.set(0); // سيؤدي إلى حلقة لا نهائية
}
});
// ✅ صحيح: استخدام computed signals بدلاً من effects للتعديلات
const resetCount = computed(() => count() > 5 ? 0 : count());
// ملاحظات هامة:
// 1. تجنب تعديل الحالة داخل effects
// 2. استخدم computed signals للحسابات المعتمدة على الحالة
// 3. كن حذراً مع الكائنات والمصفوفات عند استخدام المساواة المرجعيةرغم أن Signals تقدم العديد من المزايا، إلا أنها ليست الحل الأمثل لكل سيناريو. أحد الحالات التي يجب تجنبها هي عندما تحتاج إلى معالجة متقدمة للبيانات غير المتزامنة. على سبيل المثال، إذا كنت تعمل على تطبيق دردشة وتحتاج إلى دمج تدفقات الرسائل من مصادر متعددة مع فلترة الرسائل القديمة، فإن RxJS تبقى الخيار الأفضل. أيضاً، إذا كنت تعمل في فريق يعتمد بشكل كبير على NgRx لإدارة الحالة، فقد لا يكون الانتقال إلى Signals مفيداً على المدى القصير، خاصةً إذا كان التطبيق كبير ومعقد.
حالة أخرى يجب الحذر فيها هي عند التعامل مع مكونات خارجية لا تدعم Signals بعد. على سبيل المثال، إذا كنت تستخدم مكتبات واجهة مستخدم خارجية تعتمد على RxJS للاتصال بالحالة، فقد تواجه مشاكل في التكامل. في أحد المشاريع التي عملت عليها، اضطررنا إلى كتابة طبقة تكامل بين Signals و RxJS لتتمكن مكتبة رسوم بيانية خارجية من العمل بشكل صحيح مع الحالة الجديدة. هذا أضاف تعقيداً غير متوقع للمشروع، وكان من الأفضل في تلك الحالة البقاء على RxJS بالكامل.
مع إطلاق Angular 17، أصبحت Signals جزءاً أساسياً من الإطار، لكن الطريق أمامها لا يزال طويلاً. أحد التحسينات المتوقعة هو دمج Signals بشكل أعمق مع نظام التغيير في Angular، مما سيسمح بتحسينات كبيرة في الأداء دون الحاجة إلى كتابة أكواد إضافية. على سبيل المثال، هناك حديث عن إمكانية استخدام Signals لتحديث أجزاء محددة من شجرة DOM دون الحاجة إلى إعادة التحقق من المكونات بأكملها.
أيضاً، هناك خطط لإضافة دعم أفضل لـ Signals داخل الخدمات والـ Directives، مما سيسهل إدارة الحالة المشتركة بين المكونات دون الحاجة إلى الاعتماد على RxJS أو مكتبات خارجية. في مؤتمر Angular Nation الأخير، تم عرض نموذج أولي لميزة تسمى "Signal-based Services" التي تسمح بإنشاء خدمات تعتمد بالكامل على Signals، مما يبسط إدارة الحالة المشتركة بشكل كبير. هذا يعني أنه في المستقبل، قد لا نحتاج إلى استخدام RxJS إلا في الحالات الأكثر تعقيداً فقط.
// مثال على ما قد يبدو عليه Signal-based Service في المستقبل
import { signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
// خدمة تعتمد بالكامل على Signals
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private users = signal<User[]>([]);
private loading = signal(false);
private error = signal<string | null>(null);
// computed signals للحالة المشتقة
usersCount = computed(() => this.users().length);
hasError = computed(() => this.error() !== null);
// تحميل المستخدمين
loadUsers() {
this.loading.set(true);
this.error.set(null);
this.http.get<User[]>('/api/users').subscribe({
next: (users) => this.users.set(users),
error: (err) => this.error.set(err.message),
complete: () => this.loading.set(false)
});
}
// تحويل إلى signal للاستخدام داخل المكونات
usersSignal = toSignal(this.http.get<User[]>('/api/users'), {
initialValue: []
});
}
// استخدام الخدمة داخل مكون
@Component({
selector: 'app-users',
template: `
<div *ngIf="userService.loading()">Loading...</div>
<div *ngIf="userService.error()">Error: {{ userService.error() }}</div>
<ul>
<li *ngFor="let user of userService.users()">{{ user.name }}</li>
</ul>
<p>Total users: {{ userService.usersCount() }}</p>
`
})
export class UsersComponent {
userService = inject(UserService);
}
// ملاحظات:
// 1. الخدمة تعتمد بالكامل على Signals لإدارة الحالة
// 2. يمكن استخدام computed signals للحالة المشتقة
// 3. تحويل HttpClient إلى signal باستخدام toSignal
// 4. المكونات تعتمد على Signals مباشرة دون الحاجة إلى RxJSإذا كنت تفكر في الانتقال إلى Signals في مشروعك التالي، فهذه هي النصائح العملية التي استخلصتها من تجربتي مع عشرات المشاريع الكبيرة: ابدأ بتجربة Signals في مكونات جديدة أو صغيرة أولاً، ثم قم بتوسيع استخدامها تدريجياً. لا تحاول إعادة كتابة كل شيء دفعة واحدة، خاصةً إذا كان مشروعك يعتمد بشكل كبير على RxJS. استخدم `toSignal` و `toObservable` لدمج Signals مع الأكواد الموجودة التي تعتمد على RxJS دون الحاجة إلى إعادة كتابة كل شيء.
أيضاً، اهتم بكيفية تتبع التبعيات داخل loops ومع الكائنات المعقدة. استخدم دوال المساواة المخصصة عند الضرورة، وتجنب تعديل الحالة داخل effects. وأخيراً، لا تنسَ أن Signals ليست بديلاً كاملاً لـ RxJS، بل أداة مكملة. استخدم كل أداة في المكان المناسب لها، وستحصل على تطبيق سريع وسهل الصيانة. وإذا كنت تعمل على مشروع جديد، فابدأ باستخدام Signals منذ اليوم الأول، وستوفر على نفسك الكثير من الوقت والجهد في المستقبل.