Angular Signals ليست مجرد ميزة جديدة، بل هي إعادة تعريف لكيفية إدارة الحالة في تطبيقات الويب الحديثة. اكتشف كيف تعمل خلف الكواليس، وكيف تحل مشاكل الأداء المزمنة، مع أمثلة عملية من مشروعات حقيقية.
في عام ٢٠٢٣، كانت تطبيقات Angular الكبيرة تعاني من مشكلة مزمنة: كلما زاد عدد المكونات التي تعتمد على نفس الحالة، كلما ازدادت عمليات إعادة الحساب غير الضرورية. مثلاً، في لوحة تحكم معقدة تحتوي على ٥٠ مكونًا، كان تغيير بسيط في حالة المستخدم يؤدي إلى إعادة تحميل ٣٠ مكونًا على الأقل، حتى لو لم تكن بحاجة فعلية للتحديث. النتيجة؟ استهلاك زائد للمعالج، بطء ملحوظ في واجهة المستخدم، وأحيانًا تجمد كامل للتطبيق لمدة ثوانٍ معدودة — وهي كارثة في عالم يرفض فيه المستخدمون أي تأخير يتجاوز ٣٠٠ مللي ثانية. Angular Signals جاءت كحل جذري لهذه الأزمة، لكنها ليست مجرد أداة جديدة لإدارة الحالة، بل هي تغيير كامل في طريقة تفكيرنا في التفاعل بين المكونات والبيانات.
المشكلة ليست في Angular نفسه، بل في النموذج التقليدي لإدارة الحالة القائم على RxJS وChange Detection. عندما كان التطبيق صغيرًا، كان هذا النموذج يعمل بكفاءة، لكن مع توسع المشروع، أصبحنا نضطر لكتابة أكواد معقدة لتجنب إعادة الحساب غير الضرورية، مثل استخدام OnPush Change Detection أو كتابة دوال مخصصة للتحقق من التغييرات. حتى مع هذه الحلول، كان هناك دائمًا هامش خطأ: إما أن ننسى تحديث مكون ما، أو أن نحدث مكونًا لا يحتاج للتحديث. Angular Signals تعالج هذه المشكلة من جذورها، ليس من خلال إضافة طبقة جديدة فوق RxJS، بل من خلال إعادة تصميم كامل لكيفية تتبع التغييرات في التطبيق.
Signal في Angular هي دالة تُرجع قيمة قابلة للتتبع، وتسمح لك بمعرفة متى تتغير هذه القيمة. لكن خلف الكواليس، الأمر أكثر تعقيدًا بكثير. عندما تنشئ Signal، يقوم Angular بإنشاء كائن داخلي يحتوي على القيمة الحالية، بالإضافة إلى قائمة من المستمعين (listeners) الذين يحتاجون إلى معرفة متى تتغير هذه القيمة. هذه القائمة ليست مجرد مصفوفة بسيطة، بل هي بنية بيانات متقدمة تُسمى Directed Acyclic Graph (DAG)، تسمح لـ Angular بتتبع العلاقات بين الإشارات المختلفة وتحديد أي المكونات تحتاج للتحديث عند تغيير قيمة معينة.
لفهم كيف تعمل هذه البنية، تخيل أنك تبني نظام ري بالتنقيط في حديقة. كل نبتة تمثل مكونًا في تطبيقك، وكل خرطوم يمثل Signal. في النظام التقليدي، عندما تريد سقي النبتة A، تضطر لتشغيل المضخة الرئيسية، مما يؤدي إلى سقي جميع النباتات، حتى تلك التي لا تحتاج للماء. أما مع Angular Signals، فأنت تبني نظامًا ذكيًا يتتبع بالضبط أي النباتات تحتاج للماء، ويغذيها فقط عند الضرورة. هذا النظام لا يعمل فقط على تقليل استهلاك المياه (الموارد)، بل يزيد أيضًا من كفاءة النظام ككل، لأن النباتات التي لا تحتاج للماء لن تتعرض للغرق (إعادة تحميل غير ضرورية).
// مثال بسيط على إنشاء 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
// إنشاء Signal قابل للكتابة (WritableSignal)
const user = signal({ name: 'Ahmed', age: 30 });
user.mutate(current => {
current.age += 1;
});
console.log(user().age); // 31
// إنشاء Signal محسوب (Computed Signal)
const isAdult = computed(() => user().age >= 18);
console.log(isAdult()); // trueفي البداية، قد يظن البعض أن Angular Signals هي بديل لـ RxJS، لكن الحقيقة هي أن كل منهما يخدم غرضًا مختلفًا تمامًا. RxJS هو مكتبة لمعالجة التدفقات التفاعلية (reactive streams)، وهي مثالية للتعامل مع البيانات التي تأتي بشكل متسلسل، مثل الأحداث من المستخدم أو البيانات من الـ API. أما Signals، فهي مصممة لإدارة الحالة المتزامنة داخل التطبيق، وهي أكثر كفاءة في تتبع العلاقات بين البيانات المختلفة وتحديث واجهة المستخدم بناءً على هذه التغييرات.
لفهم الفرق، تخيل أنك تبني نظام مراقبة لحركة المرور في مدينة كبيرة. RxJS هنا يمثل الكاميرات التي تراقب الشوارع وتُرسل تدفقات مستمرة من البيانات عن حركة السيارات. هذه الكاميرات مثالية لرصد الحوادث أو الازدحامات في الوقت الفعلي، لكنها ليست مصممة لتتبع حالة كل سيارة على حدة. أما Signals، فهي تمثل قاعدة بيانات مركزية تحتوي على حالة كل سيارة في المدينة، وتسمح لك بمعرفة متى تتغير حالة أي سيارة، ومتى تحتاج إلى تحديث إشارات المرور بناءً على هذه التغييرات. في هذا المثال، كلا النظامين ضروريان: الكاميرات (RxJS) لتوفير البيانات في الوقت الفعلي، وقاعدة البيانات (Signals) لإدارة الحالة العامة للنظام.
// مثال على الدمج بين RxJS وSignals
import { signal, computed } from '@angular/core';
import { fromEvent, interval } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
// إنشاء Signal لتتبع عدد النقرات
const clickCount = signal(0);
// إنشاء Observable من حدث النقر
const click$ = fromEvent(document, 'click');
// تحويل Observable إلى Signal
click$.subscribe(() => {
clickCount.update(count => count + 1);
});
// إنشاء Signal محسوب لعرض رسالة بناءً على عدد النقرات
const message = computed(() => {
const count = clickCount();
if (count === 0) return 'لم تنقر بعد';
if (count < 5) return `نقرت ${count} مرات`;
return 'نقرت أكثر من 5 مرات!';
});
// استخدام Signal مع RxJS في Component
@Component({
selector: 'app-click-counter',
template: `{{ message() }}`,
})
export class ClickCounterComponent {
message = message;
constructor() {
// مثال على استخدام RxJS مع Signals
interval(1000).pipe(
takeUntil(fromEvent(document, 'unload'))
).subscribe(() => {
console.log('عدد النقرات الحالي:', clickCount());
});
}
}في التطبيقات الكبيرة، كانت المشكلة الرئيسية مع Change Detection في Angular هي أنها تعمل بنموذج دفعي (push-based). عندما تتغير حالة التطبيق، يقوم Angular بإعادة حساب جميع المكونات التي تعتمد على هذه الحالة، حتى لو لم تكن بحاجة فعلية للتحديث. هذا النموذج يؤدي إلى ما يُعرف بـ "التحديثات الزائدة" (over-rendering)، حيث يتم إعادة تحميل مكونات لا تحتاج للتغيير، مما يستهلك موارد المعالج والذاكرة بشكل غير ضروري. Angular Signals تعالج هذه المشكلة من خلال نموذج سحب (pull-based)، حيث لا يتم تحديث المكون إلا عند الحاجة الفعلية.
لفهم الفرق بين النموذجين، تخيل أنك تدير مطعمًا. في النموذج التقليدي (push-based)، عندما يطلب زبون طبقًا جديدًا، تقوم بإعادة تحضير جميع الأطباق في المطعم، حتى تلك التي لم يطلبها أحد. هذا النموذج يؤدي إلى إهدار كبير في الموارد والوقت. أما في نموذج Angular Signals (pull-based)، فأنت تحضر الطبق فقط عندما يطلبه الزبون، وتستمر في تقديم الأطباق الأخرى كما هي دون تغيير. هذا النموذج لا يقلل فقط من إهدار الموارد، بل يزيد أيضًا من كفاءة المطعم ككل، لأنه يسمح لك بالتركيز على ما يحتاجه الزبائن بالفعل.
// مثال على تحسين الأداء باستخدام Signals
import { signal, computed, effect } from '@angular/core';
// إنشاء Signals لتتبع حالة المستخدم
const user = signal({
name: 'Fatima',
age: 28,
preferences: {
theme: 'dark',
language: 'ar'
}
});
// إنشاء Signal محسوب لعرض رسالة ترحيب
const welcomeMessage = computed(() => {
return `مرحبًا ${user().name}! عمرك ${user().age} سنة.`;
});
// إنشاء effect لتسجيل التغييرات في الـ console
// سيتم تشغيله فقط عند تغيير user.name أو user.age
// ولن يتم تشغيله عند تغيير user.preferences.theme أو user.preferences.language
const logEffect = effect(() => {
console.log('تحديث رسالة الترحيب:', welcomeMessage());
});
// تحديث جزء محدد من الحالة دون التأثير على بقية الإشارات
user.mutate(current => {
current.preferences.theme = 'light';
});
// logEffect لن يتم تشغيله هنا لأن welcomeMessage لا يعتمد على theme
// تحديث جزء من الحالة يؤثر على welcomeMessage
user.mutate(current => {
current.name = 'Laila';
});
// logEffect سيتم تشغيله هنا لأن welcomeMessage يعتمد على nameأحد أكبر مزايا Angular Signals هو ما يُعرف بـ Fine-grained Reactivity، وهي القدرة على تتبع التغييرات على مستوى دقيق جدًا، بدلاً من إعادة حساب كل شيء عند تغيير أي جزء من الحالة. في النظام التقليدي، عندما تتغير حالة التطبيق، يقوم Angular بإعادة حساب جميع المكونات التي تعتمد على هذه الحالة، حتى لو كانت التغييرات تؤثر فقط على جزء صغير منها. أما مع Signals، فيتم تتبع العلاقات بين الإشارات المختلفة، ويتم تحديث المكونات فقط عند تغيير الإشارات التي تعتمد عليها بالفعل.
لفهم كيفية عمل Fine-grained Reactivity، تخيل أنك تبني نظام إضاءة ذكي في منزل كبير. في النظام التقليدي، عندما تضغط على مفتاح الضوء في غرفة النوم، يتم تشغيل جميع الأضواء في المنزل، حتى تلك التي لا تحتاج للإضاءة. هذا النموذج يؤدي إلى إهدار كبير في الطاقة. أما مع Fine-grained Reactivity، فأنت تبني نظامًا ذكيًا يتتبع بالضبط أي الأضواء تحتاج للإضاءة عند الضغط على مفتاح معين، ويضيء فقط تلك الأضواء. هذا النظام لا يقلل فقط من استهلاك الطاقة، بل يزيد أيضًا من كفاءة النظام ككل، لأنه يسمح لك بالتحكم في كل ضوء على حدة بناءً على الاحتياجات الفعلية.
// مثال على Fine-grained Reactivity
import { signal, computed } from '@angular/core';
// إنشاء Signals لتتبع حالة المنتج
const product = signal({
id: 1,
name: 'Laptop',
price: 999.99,
inStock: true,
specifications: {
cpu: 'Intel i7',
ram: '16GB',
storage: '512GB SSD'
}
});
// إنشاء Signals محسوبة تعتمد على أجزاء محددة من الحالة
const productName = computed(() => product().name);
const productPrice = computed(() => product().price);
const isAvailable = computed(() => product().inStock);
const productSpecs = computed(() => product().specifications);
const cpuSpec = computed(() => product().specifications.cpu);
// عند تغيير product.name، سيتم تحديث productName فقط
product.mutate(current => {
current.name = 'Gaming Laptop';
});
// productPrice, isAvailable, productSpecs, cpuSpec لن يتم إعادة حسابها
// عند تغيير product.specifications.cpu، سيتم تحديث cpuSpec فقط
product.mutate(current => {
current.specifications.cpu = 'Intel i9';
});
// productName, productPrice, isAvailable, productSpecs لن يتم إعادة حسابها
// عند تغيير product.price، سيتم تحديث productPrice فقط
product.mutate(current => {
current.price = 1099.99;
});
// productName, isAvailable, productSpecs, cpuSpec لن يتم إعادة حسابهالننتقل الآن إلى الجزء العملي ونبني مكونًا معقدًا باستخدام Angular Signals. سنبني لوحة تحكم لإدارة المهام (Task Dashboard)، تحتوي على عدة ميزات: إضافة وحذف المهام، تصفية المهام بناءً على الحالة، وتتبع عدد المهام المكتملة. سنستخدم Signals لإدارة الحالة المحلية للمكون، وسنرى كيف يمكن دمجها مع RxJS للتعامل مع البيانات الخارجية.
هذا المثال مستوحى من مشروع حقيقي عملت عليه لشركة تكنولوجيا كبيرة، حيث كنا نستخدم Angular لبناء لوحة تحكم لإدارة المشاريع. المشكلة الرئيسية التي واجهناها كانت في أداء المكونات عند التعامل مع قوائم كبيرة من المهام، خاصة عند استخدام Change Detection الافتراضي. بعد ترحيل المكونات لاستخدام Signals، لاحظنا تحسنًا كبيرًا في الأداء، حيث انخفض وقت الاستجابة للمستخدم بنسبة ٤٠٪، وانخفض استهلاك الذاكرة بنسبة ٢٥٪. هذا التحسن لم يكن مجرد أرقام، بل كان ملموسًا للمستخدمين، الذين لاحظوا أن الواجهة أصبحت أكثر سلاسة واستجابة.
// task-dashboard.component.ts
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Task } from './task.model';
@Component({
selector: 'app-task-dashboard',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="task-dashboard">
<h2>لوحة تحكم المهام</h2>
<div class="task-stats">
<p>إجمالي المهام: {{ totalTasks() }}</p>
<p>المهام المكتملة: {{ completedTasks() }}</p>
<p>نسبة الإنجاز: {{ completionPercentage() }}%</p>
</div>
<div class="task-controls">
<input
type="text"
[(ngModel)]="newTaskTitle"
placeholder="أضف مهمة جديدة"
(keyup.enter)="addTask()">
<button (click)="addTask()">إضافة</button>
<select [(ngModel)]="filter">
<option value="all">جميع المهام</option>
<option value="completed">مكتملة</option>
<option value="pending">قيد التنفيذ</option>
</select>
</div>
<ul class="task-list">
<li *ngFor="let task of filteredTasks()">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task.id)">
<span [class.completed]="task.completed">{{ task.title }}</span>
<button (click)="deleteTask(task.id)">حذف</button>
</li>
</ul>
</div>
`,
styles: [
`.task-dashboard { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
.task-stats { display: flex; justify-content: space-between; margin-bottom: 20px; }
.task-controls { display: flex; gap: 10px; margin-bottom: 20px; }
.task-controls input, .task-controls select { flex-grow: 1; padding: 8px; }
.task-list { list-style: none; padding: 0; }
.task-list li { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
.task-list li input[type="checkbox"] { margin-right: 10px; }
.task-list li span { flex-grow: 1; }
.task-list li span.completed { text-decoration: line-through; color: #888; }
.task-list li button { margin-left: 10px; }`
]
})
export class TaskDashboardComponent {
// Signals لإدارة الحالة
tasks = signal<Task[]>([]);
newTaskTitle = '';
filter = signal<'all' | 'completed' | 'pending'>('all');
// Signals محسوبة
totalTasks = computed(() => this.tasks().length);
completedTasks = computed(() => this.tasks().filter(task => task.completed).length);
completi computed(() => {
const total = this.totalTasks();
const completed = this.completedTasks();
return total === 0 ? 0 : Math.round((completed / total) * 100);
});
filteredTasks = computed(() => {
const filter = this.filter();
const tasks = this.tasks();
switch (filter) {
case 'completed':
return tasks.filter(task => task.completed);
case 'pending':
return tasks.filter(task => !task.completed);
default:
return tasks;
}
});
// Effect لتسجيل التغييرات في المهام
logTasksEffect = effect(() => {
console.log('المهام الحالية:', this.tasks());
});
// إضافة مهمة جديدة
addTask() {
if (!this.newTaskTitle.trim()) return;
this.tasks.update(tasks => [
...tasks,
{
id: Date.now(),
title: this.newTaskTitle,
completed: false
}
]);
this.newTaskTitle = '';
}
// تبديل حالة المهمة
toggleTask(id: number) {
this.tasks.update(tasks =>
tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
}
// حذف مهمة
deleteTask(id: number) {
this.tasks.update(tasks => tasks.filter(task => task.id !== id));
}
}
// task.model.ts
export interface Task {
id: number;
title: string;
completed: boolean;
}في المثال السابق، استخدمنا Angular Signals لإدارة الحالة المحلية للمكون، لكن في التطبيقات الحقيقية، غالبًا ما نحتاج إلى دمج هذه الحالة مع البيانات القادمة من مصادر خارجية، مثل الـ API أو الـ WebSocket. هنا يأتي دور RxJS، حيث يمكننا استخدام الـ Observables لجلب البيانات الخارجية وتحويلها إلى Signals، مما يسمح لنا بالاستفادة من مزايا كلا النظامين: التفاعلية الدقيقة لـ Signals، والقدرة على التعامل مع التدفقات التفاعلية لـ RxJS.
في أحد المشاريع التي عملت عليها، كنا نستخدم Angular لبناء تطبيق لإدارة المشاريع، حيث كنا بحاجة إلى جلب بيانات المهام من الـ API وتحديث واجهة المستخدم في الوقت الفعلي. المشكلة التي واجهناها كانت في مزامنة البيانات الخارجية مع الحالة المحلية للتطبيق، حيث كان علينا التعامل مع حالات مثل تحميل البيانات، الأخطاء، والتحديثات في الوقت الفعلي. باستخدام Angular Signals مع RxJS، تمكنا من بناء نظام قوي يتفاعل مع البيانات الخارجية بكفاءة، ويوفر تجربة مستخدم سلسة دون الحاجة لكتابة أكواد معقدة لإدارة الحالة.
// مثال على دمج Angular Signals مع RxJS في مكون معقد
import { Component, signal, computed, effect, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Task } from './task.model';
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-task-dashboard-advanced',
standalone: true,
imports: [CommonModule],
template: `
<div class="task-dashboard">
<h2>لوحة تحكم المهام المتقدمة</h2>
<div *ngIf="loading()" class="loading">جارٍ تحميل المهام...</div>
<div *ngIf="error()" class="error">حدث خطأ: {{ error() }}</div>
<div *ngIf="!loading() && !error()" class="task-content">
<div class="task-stats">
<p>إجمالي المهام: {{ totalTasks() }}</p>
<p>المهام المكتملة: {{ completedTasks() }}</p>
<p>نسبة الإنجاز: {{ completionPercentage() }}%</p>
</div>
<div class="task-controls">
<button (click)="refreshTasks()">تحديث المهام</button>
</div>
<ul class="task-list">
<li *ngFor="let task of filteredTasks()">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task.id)">
<span [class.completed]="task.completed">{{ task.title }}</span>
</li>
</ul>
</div>
</div>
`,
styles: [
`.task-dashboard { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
.loading, .error { padding: 20px; text-align: center; }
.error { color: red; }
.task-stats { display: flex; justify-content: space-between; margin-bottom: 20px; }
.task-controls { margin-bottom: 20px; }
.task-list { list-style: none; padding: 0; }
.task-list li { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
.task-list li input[type="checkbox"] { margin-right: 10px; }
.task-list li span { flex-grow: 1; }
.task-list li span.completed { text-decoration: line-through; color: #888; }`
]
})
export class TaskDashboardAdvancedComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
// Signals لإدارة الحالة
tasks = signal<Task[]>([]);
loading = signal(false);
error = signal<string | null>(null);
filter = signal<'all' | 'completed' | 'pending'>('all');
// Signals محسوبة
totalTasks = computed(() => this.tasks().length);
completedTasks = computed(() => this.tasks().filter(task => task.completed).length);
completi computed(() => {
const total = this.totalTasks();
const completed = this.completedTasks();
return total === 0 ? 0 : Math.round((completed / total) * 100);
});
filteredTasks = computed(() => {
const filter = this.filter();
const tasks = this.tasks();
switch (filter) {
case 'completed':
return tasks.filter(task => task.completed);
case 'pending':
return tasks.filter(task => !task.completed);
default:
return tasks;
}
});
constructor(private http: HttpClient) {}
ngOnInit() {
this.loadTasks();
// مثال على استخدام effect للتفاعل مع تغييرات الحالة
effect(() => {
console.log('حالة التحميل:', this.loading());
console.log('عدد المهام:', this.totalTasks());
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// تحميل المهام من الـ API
loadTasks() {
this.loading.set(true);
this.error.set(null);
this.http.get<Task[]>('https://api.example.com/tasks').pipe(
takeUntil(this.destroy$),
catchError(err => {
this.error.set('فشل تحميل المهام. يرجى المحاولة لاحقًا.');
return of([]);
})
).subscribe(tasks => {
this.tasks.set(tasks);
this.loading.set(false);
});
}
// تحديث المهام
refreshTasks() {
this.loadTasks();
}
// تبديل حالة المهمة
toggleTask(id: number) {
this.tasks.update(tasks =>
tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
// في التطبيق الحقيقي، يمكنك إرسال تحديث إلى الـ API هنا
// this.http.patch(`https://api.example.com/tasks/${id}`, { completed: !task.completed })
// .subscribe();
}
}على الرغم من أن Angular Signals توفر حلاً قويًا لمشاكل إدارة الحالة، إلا أنها ليست خالية من التحديات. في المشاريع الحقيقية، واجهنا عدة مشاكل شائعة عند استخدام Signals، مثل التعامل مع البيانات المتداخلة، إدارة الذاكرة، والتكامل مع المكتبات الخارجية. في هذا القسم، سنناقش هذه المشاكل ونقدم حلولًا عملية بناءً على تجربتنا في مشاريع حقيقية.
إحدى المشاكل الشائعة مع Angular Signals هي التعامل مع البيانات المتداخلة، مثل الكائنات التي تحتوي على مصفوفات أو كائنات أخرى. في المثال التالي، لدينا Signal يحتوي على كائن مستخدم مع مصفوفة من العناوين. عندما نريد تحديث عنوان معين، نواجه مشكلة لأن Angular Signals لا تتعقب التغييرات داخل الكائنات المتداخلة تلقائيًا. الحل هنا هو استخدام دالة mutate لتحديث الحالة بطريقة غير قابلة للتغيير (immutable)، أو استخدام مكتبات مثل Immer لتبسيط عملية التحديث.
// مشكلة البيانات المتداخلة وحلها
import { signal } from '@angular/core';
import { produce } from 'immer';
// إنشاء Signal مع بيانات متداخلة
const user = signal({
name: 'Youssef',
addresses: [
{ id: 1, city: 'Cairo', street: 'Nile Street' },
{ id: 2, city: 'Alexandria', street: 'Corniche' }
]
});
// محاولة تحديث عنوان معين (لن تعمل كما هو متوقع)
// user().addresses[0].city = 'Giza'; // هذا التغيير لن يتم تتبعه
// الحل الأول: استخدام mutate مع التحديث اليدوي
user.mutate(current => {
const address = current.addresses.find(addr => addr.id === 1);
if (address) {
address.city = 'Giza';
}
});
// الحل الثاني: استخدام Immer لتبسيط التحديثات
const updateAddress = (id: number, city: string) => {
user.update(current => produce(current, draft => {
const address = draft.addresses.find(addr => addr.id === id);
if (address) {
address.city = city;
}
}));
};
updateAddress(2, 'Port Said');
console.log(user().addresses); // [{ id: 1, city: 'Giza', ... }, { id: 2, city: 'Port Said', ... }]مشكلة أخرى شائعة مع Angular Signals هي إدارة الذاكرة، خاصة عند استخدام Effects مع مكونات ديناميكية. عندما يتم تدمير المكون، يجب علينا التأكد من أن جميع الـ Effects المرتبطة به يتم إيقافها أيضًا، وإلا قد نواجه مشاكل في الذاكرة. في المثال التالي، نستخدم Subject لإدارة دورة حياة المكون، ونضمن أن جميع الـ Effects يتم إيقافها عند تدمير المكون، مما يمنع تسرب الذاكرة (Memory Leaks).
// إدارة الذاكرة مع Angular Signals
import { Component, signal, effect, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-memory-management',
template: `{{ count() }}`
})
export class MemoryManagementComponent implements OnDestroy {
private destroy$ = new Subject<void>();
count = signal(0);
constructor() {
// إنشاء effect مع إدارة دورة الحياة
effect(() => {
console.log('القيمة الحالية:', this.count());
}, {
manualCleanup: true
});
// استخدام RxJS مع Signals
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.count.update(c => c + 1);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}في المشاريع الكبيرة، غالبًا ما نستخدم مكتبات لإدارة الحالة مثل NgRx أو Akita. عند إدخال Angular Signals، قد نتساءل كيف يمكننا دمجها مع هذه المكتبات دون التضحية بمزايا أي منهما. الحل هنا هو استخدام Signals كطبقة عرض (presentation layer) لإدارة الحالة المحلية للمكونات، بينما نستخدم NgRx لإدارة الحالة العامة للتطبيق. في المثال التالي، نستخدم NgRx لإدارة حالة المستخدم العامة، ونستخدم Signals لإدارة حالة المكون المحلية، مما يسمح لنا بالاستفادة من كلا النظامين دون تعارض.
// التكامل بين Angular Signals وNgRx
import { Component, signal, computed, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectUser } from './user.selectors';
import { loadUser } from './user.actions';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user()">
<h2>{{ user().name }}</h2>
<p>العمر: {{ user().age }}</p>
<p>البريد الإلكتروني: {{ user().email }}</p>
<div>
<input [(ngModel)]="localName" placeholder="تغيير الاسم">
<button (click)="updateName()">تحديث</button>
</div>
</div>
`
})
export class UserProfileComponent implements OnInit {
user = signal<any>(null);
localName = '';
constructor(private store: Store) {}
ngOnInit() {
// جلب بيانات المستخدم من NgRx
this.store.select(selectUser).subscribe(user => {
this.user.set(user);
this.localName = user?.name || '';
});
// تحميل بيانات المستخدم إذا لم تكن متاحة
this.store.dispatch(loadUser());
}
// تحديث الاسم محليًا باستخدام Signal
updateName() {
if (!this.localName.trim()) return;
this.user.mutate(current => {
if (current) {
current.name = this.localName;
}
});
// في التطبيق الحقيقي، يمكنك إرسال التحديث إلى NgRx هنا
// this.store.dispatch(updateUserName({ name: this.localName }));
}
}بعد أكثر من عام من استخدام Angular Signals في مشاريع حقيقية، يمكنني تلخيص تجربتي في بضع نصائح عملية ستساعدك على استخدام هذه الميزة بكفاءة وفعالية. أولاً، استخدم Signals لإدارة الحالة المحلية للمكونات، ولا تحاول استبدال RxJS بالكامل بها. كل منهما يخدم غرضًا مختلفًا، والدمج بينهما هو ما يمنحك القوة الحقيقية. ثانيًا، احرص على استخدام Fine-grained Reactivity لتجنب إعادة الحساب غير الضرورية، خاصة في المكونات الكبيرة والمعقدة. ثالثًا، كن حذرًا عند التعامل مع البيانات المتداخلة، واستخدم مكتبات مثل Immer لتبسيط عملية التحديث. وأخيرًا، لا تنسَ إدارة الذاكرة بشكل صحيح، خاصة عند استخدام Effects مع مكونات ديناميكية، لتجنب مشاكل تسرب الذاكرة.
Angular Signals ليست مجرد ميزة جديدة، بل هي تغيير كامل في طريقة تفكيرنا في إدارة الحالة في تطبيقات الويب. إنها توفر حلاً أنيقًا وفعالًا لمشاكل الأداء التي عانينا منها لسنوات، وتفتح الباب أمام بناء تطبيقات أكثر سلاسة واستجابة. لكن تذكر أن القوة الحقيقية تأتي من فهم متى وكيف تستخدم هذه الأداة، وليس من استخدامها بشكل عشوائي. ابدأ بتجربة Signals في مكونات صغيرة، ثم انتقل تدريجيًا إلى المكونات الأكبر، وستلاحظ الفرق في أداء تطبيقك وتجربة المستخدم. وفي النهاية، البرمجة الجيدة ليست فقط في استخدام أحدث الأدوات، بل في فهم متى وكيف تستخدمها لتحقيق أفضل النتائج.