اكتشف كيف تُحدث Angular Signals نقلة نوعية في إدارة الحالة داخل تطبيقات Angular، مع شرح تقني عميق وأمثلة عملية تكشف أسرار الأداء والتحسين خلف الكواليس.
تخيل أنك تعمل على لوحة تحكم معقدة في تطبيق Angular، حيث يتغير جزء واحد من البيانات في واجهة المستخدم، فيجب تحديث خمسة مكونات مختلفة في الوقت نفسه. في السابق، كنت ستعتمد على RxJS وSubjects أو حتى خدمات مشتركة مع BehaviorSubject، لكنك سرعان ما تجد نفسك تغرق في دوامة من الـ Subscriptions المتداخلة، والـ Memory Leaks التي لا تنتهي، والـ Change Detection الذي يعمل بشكل عشوائي وكأنك تلعب الروليت الروسي مع أداء التطبيق. المشكلة ليست فقط في التعقيد، بل في أن Angular نفسه لا يفهم حقاً متى يجب تحديث الواجهة، فيلجأ إلى الـ Zone.js لتشغيل دورة الـ Change Detection بأكملها حتى لو كان التغيير بسيطاً مثل تحديث رقم واحد في الـ DOM. هنا تأتي Angular Signals كحل ثوري، ليس لأنها تضيف مجرد أداة جديدة، بل لأنها تعيد تعريف علاقة Angular بالبيانات والتحديثات من جذورها.
في قلب Angular Signals يكمن مفهوم بسيط لكنه قوي: الإشارة (Signal). الـ Signal هو كائن يمثل قيمة متغيرة يمكن مراقبتها، وعندما تتغير هذه القيمة، تُعلم Angular تلقائياً بأن هناك جزءاً من الواجهة يحتاج إلى التحديث. لكن ما يميزها حقاً هو أنها تعمل على مستوى الـ Fine-Grained Reactivity، أي أنها تعرف بالضبط أي جزء من الـ DOM يحتاج إلى التحديث، بدلاً من إعادة رسم شجرة المكونات بأكملها. هذا ليس مجرد تحسين للأداء، بل هو تغيير جذري في كيفية تعامل Angular مع البيانات، يشبه الانتقال من قيادة سيارة تعمل بالبنزين إلى أخرى تعمل بالكهرباء – نفس الهدف، لكن الآلية مختلفة تماماً وأكثر كفاءة.
لفهم أهمية Angular Signals، يجب أولاً أن نفهم العيوب الكبيرة في النظام القديم. في Angular التقليدي، تعتمد إدارة الحالة بشكل أساسي على RxJS، وهي مكتبة رائعة بلا شك، لكنها ليست مصممة خصيصاً لإدارة الحالة داخل تطبيقات الـ Frontend. عندما تستخدم BehaviorSubject مثلاً لتخزين حالة التطبيق، فأنت في الواقع تنشئ تدفقاً من البيانات يمكن للمكونات الاشتراك فيه عبر الـ Subscribe. المشكلة هنا أن كل اشتراك هو نقطة ضعف محتملة: إذا نسيت إلغاء الاشتراك، ستحصل على Memory Leak، وإذا كان لديك مكونات كثيرة تشترك في نفس الـ Subject، فسيتم تشغيل دورة الـ Change Detection لكل مكون حتى لو كان التغيير غير ذي صلة به.
خذ مثلاً تطبيقاً يحتوي على قائمة مهام (Todo List) حيث يمكن للمستخدم إضافة أو حذف مهام. في النظام القديم، قد تستخدم خدمة مشتركة تحتوي على BehaviorSubject لتخزين القائمة، ثم تشترك كل مكون يحتاج إلى عرض هذه القائمة في هذا الـ Subject. عندما يضيف المستخدم مهمة جديدة، يتم تحديث الـ Subject، مما يؤدي إلى تشغيل دورة الـ Change Detection لكل مكون مشترك، حتى لو كان المكون لا يعرض سوى عدد المهام وليس القائمة نفسها. هذا يشبه إرسال رسالة بريدية إلى كل سكان المدينة ليخبرهم أن شخصاً واحداً قد تلقى طرداً – غير فعال على الإطلاق. بالإضافة إلى ذلك، إذا كان لديك مكونات متداخلة تعتمد على نفس البيانات، فقد ينتهي بك الأمر بـ Change Detection Loops لا نهائي، حيث يحاول Angular تحديث الواجهة بشكل متكرر دون توقف، مما يؤدي إلى تجميد المتصفح.
// مثال تقليدي باستخدام BehaviorSubject في Angular
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private todosSubject = new BehaviorSubject<string[]>([]);
todos$ = this.todosSubject.asObservable();
addTodo(todo: string) {
const currentTodos = this.todosSubject.getValue();
this.todosSubject.next([...currentTodos, todo]);
}
}
// في المكون
import { Component, OnInit, OnDestroy } from '@angular/core';
import { TodoService } from './todo.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-todo-list',
template: `...
`
})
export class TodoListComponent implements OnInit, OnDestroy {
todos: string[] = [];
private subscription: Subscription;
constructor(private todoService: TodoService) {}
ngOnInit() {
this.subscription = this.todoService.todos$.subscribe(todos => {
this.todos = todos;
});
}
ngOnDestroy() {
this.subscription.unsubscribe(); // نسيان هذا يسبب Memory Leak
}
}الـ Memory Leaks ليست المشكلة الوحيدة هنا. حتى لو تذكرت إلغاء الاشتراكات، فإن أداء التطبيق سيظل يعاني لأن Angular لا يعرف بالضبط أي جزء من الواجهة يحتاج إلى التحديث. عندما يتغير الـ BehaviorSubject، يتم تشغيل دورة الـ Change Detection لكل مكون في التطبيق، مما يؤدي إلى إعادة حساب كل تعبير في الـ Template، حتى لو كان التغيير لا يؤثر على معظم المكونات. هذا النهج يشبه إعادة بناء منزل كامل لأنك غيرت لون باب واحد – مكلف وغير ضروري.
الـ Signal في Angular هو كائن يحتوي على قيمة ويمكن مراقبته. عندما تتغير هذه القيمة، تُعلم Angular بأن هناك تحديثاً مطلوباً، لكن بدلاً من تشغيل دورة الـ Change Detection الكاملة، تستخدم Angular نظاماً ذكياً لتتبع الاعتمادات (Dependencies) بين الـ Signals والمكونات. هذا النظام يعتمد على مفهوم الـ Effect Graph، حيث يتم بناء رسم بياني يوضح العلاقة بين الـ Signals والمكونات التي تعتمد عليها. عندما يتغير الـ Signal، تتبع Angular هذا الرسم البياني لتحديث المكونات المتأثرة فقط، دون لمس بقية التطبيق.
لفهم هذا بشكل أعمق، تخيل أنك تعمل في مكتبة ضخمة وتحتاج إلى تتبع الكتب التي يستعيرها الزوار. في النظام القديم، كنت ستضطر إلى فحص كل كتاب في المكتبة لمعرفة ما إذا كان قد تم استعارته أم لا، حتى لو كان التغيير يتعلق بكتاب واحد فقط. أما مع Angular Signals، فأنت تبني نظاماً ذكياً حيث تعرف بالضبط من استعار أي كتاب، وعندما يعود الكتاب، تعلم من يجب إعلامه بالتغيير دون الحاجة إلى فحص المكتبة بأكملها. هذا هو جوهر الـ Fine-Grained Reactivity – معرفة دقيقة بمن يحتاج إلى التحديث ومن لا يحتاج.
// مثال بسيط باستخدام Angular Signals
import { signal, computed, effect } from '@angular/core';
// إنشاء Signal
const count = signal(0);
// إنشاء computed Signal يعتمد على count
const doubleCount = computed(() => count() * 2);
// استخدام effect لمراقبة التغييرات
const disposeEffect = effect(() => {
console.log(`Count changed to: ${count()}, Double is: ${doubleCount()}`);
});
// تغيير قيمة Signal
count.set(5); // سيطبع: Count changed to: 5, Double is: 10
count.update(v => v + 1); // سيطبع: Count changed to: 6, Double is: 12
// التخلص من effect عند عدم الحاجة
// disposeEffect();في هذا المثال، نرى ثلاثة مفاهيم رئيسية في Angular Signals: الـ signal الذي يخزن القيمة، والـ computed الذي يعتمد على signal آخر وينفذ حساباً عند تغييره، والـ effect الذي ينفذ إجراءً عند تغيير الـ signal. ما يحدث خلف الكواليس هو أن Angular تبني رسم بيانياً للاعتمادات. عندما يتغير count، تعرف Angular تلقائياً أن doubleCount يعتمد عليه، وبالتالي يجب إعادة حسابه، كما تعرف أن هناك effect يعتمد على كلاهما، وبالتالي يجب تشغيله. هذا الرسم البياني يتم بناؤه وتحديثه ديناميكياً، مما يعني أن Angular دائماً على دراية بالعلاقات بين الـ Signals والمكونات دون الحاجة إلى إعادة فحص التطبيق بأكمله.
أحد أكبر التحديات في أنظمة الـ Reactivity هو التعامل مع الـ Glitches، وهي حالات مؤقتة غير صحيحة تحدث أثناء تحديث القيم المعتمدة على بعضها البعض. تخيل مثلاً أنك تملك متغيرين، A و B، حيث يعتمد B على A. إذا قمت بتحديث A، ثم قمت بقراءة B قبل أن ينتهي تحديثه، فقد تحصل على قيمة غير صحيحة لـ B. في الأنظمة التقليدية، قد تحتاج إلى استخدام تقنيات مثل الـ Scheduling أو الـ Batching لتجنب هذه المشكلة، لكن Angular Signals تعالجها بشكل تلقائي من خلال نظام الـ Glitch-Free Updates.
لفهم كيف يعمل هذا، تخيل أنك تطبخ طبقين معتمدين على بعضهما: صلصة تعتمد على كمية الملح، وطبق رئيسي يعتمد على الصلصة. إذا قمت بتغيير كمية الملح، يجب أن تنتظر حتى تكتمل الصلصة قبل أن تبدأ في إعداد الطبق الرئيسي. Angular Signals تفعل شيئاً مشابهاً: عندما يتغير signal، تقوم بتحديث جميع الـ computed signals المعتمدة عليه بشكل متسلسل، وتؤجل قراءة أي قيمة حتى تكتمل جميع التحديثات. هذا يضمن أنك لن تحصل على قيم مؤقتة غير صحيحة، حتى في السيناريوهات المعقدة حيث تعتمد عدة signals على بعضها البعض.
// مثال على Glitch-Free Updates في Angular Signals
const price = signal(10);
const quantity = signal(2);
const taxRate = signal(0.1);
// computed signals معقدة تعتمد على بعضها
const subtotal = computed(() => price() * quantity());
const tax = computed(() => subtotal() * taxRate());
const total = computed(() => subtotal() + tax());
// تغيير سعر المنتج
price.set(20);
// في هذه اللحظة، جميع الـ computed signals قد تم تحديثها بشكل صحيح
console.log(total()); // 44 (20 * 2 + 4) وليس قيمة مؤقتة غير صحيحةلننتقل من النظرية إلى التطبيق العملي. سنبني جزءاً من متجر إلكتروني باستخدام Angular Signals لإدارة حالة سلة التسوق. هذا المثال سيوضح كيف يمكن لـ Signals تبسيط إدارة الحالة المعقدة وجعل الكود أكثر وضوحاً وأداءً أفضل. في المتاجر الإلكترونية، إدارة حالة سلة التسوق هي واحدة من أكثر المهام تعقيداً، حيث يجب تتبع المنتجات المضافة، الكميات، الأسعار، الخصومات، وحالة الدفع، مع ضمان تحديث الواجهة بشكل فوري عند أي تغيير.
في النظام التقليدي، قد تستخدم خدمة مشتركة تحتوي على BehaviorSubject لتخزين حالة السلة، ثم تشترك كل مكون يحتاج إلى هذه البيانات في الـ Subject. لكن هذا النهج يؤدي إلى مشاكل عديدة: أولاً، يجب عليك إدارة الاشتراكات يدوياً لتجنب الـ Memory Leaks. ثانياً، أي تغيير في السلة سيؤدي إلى تشغيل دورة الـ Change Detection لكل مكون مشترك، حتى لو كان التغيير لا يؤثر عليه. ثالثاً، الكود يصبح معقداً بسرعة، خاصة عندما تحتاج إلى حساب قيم مشتقة مثل الإجمالي أو الخصومات.
// نموذج المنتج
interface Product {
id: number;
name: string;
price: number;
image: string;
}
// نموذج عنصر السلة
interface CartItem {
product: Product;
quantity: number;
}
// خدمة إدارة سلة التسوق باستخدام Angular Signals
import { Injectable, signal, computed } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CartService {
// Signal لتخزين عناصر السلة
private cartItems = signal<CartItem[]>([]);
// computed signals للقيم المشتقة
totalItems = computed(() => this.cartItems().reduce((sum, item) => sum + item.quantity, 0));
subtotal = computed(() => this.cartItems().reduce((sum, item) => sum + (item.product.price * item.quantity), 0));
tax = computed(() => this.subtotal() * 0.1); // افترض ضريبة 10%
total = computed(() => this.subtotal() + this.tax());
// إضافة منتج إلى السلة
addToCart(product: Product) {
this.cartItems.update(items => {
const existingItem = items.find(item => item.product.id === product.id);
if (existingItem) {
return items.map(item =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
return [...items, { product, quantity: 1 }];
}
});
}
// إزالة منتج من السلة
removeFromCart(productId: number) {
this.cartItems.update(items => items.filter(item => item.product.id !== productId));
}
// تحديث كمية المنتج
updateQuantity(productId: number, quantity: number) {
if (quantity <= 0) {
this.removeFromCart(productId);
return;
}
this.cartItems.update(items =>
items.map(item =>
item.product.id === productId ? { ...item, quantity } : item
)
);
}
}
// مكون عرض السلة
import { Component } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-cart',
template: `
<div *ngIf="cartService.totalItems() > 0; else emptyCart">
<h2>سلة التسوق ({{ cartService.totalItems() }} منتجات)</h2>
<ul>
<li *ngFor="let item of cartService.cartItems()">
{{ item.product.name }} - {{ item.product.price | currency }} ×
<input type="number" [value]="item.quantity"
(change)="cartService.updateQuantity(item.product.id, $event.target.valueAsNumber)">
= {{ item.product.price * item.quantity | currency }}
<button (click)="cartService.removeFromCart(item.product.id)">إزالة</button>
</li>
</ul>
<div>المجموع الفرعي: {{ cartService.subtotal() | currency }}</div>
<div>الضريبة: {{ cartService.tax() | currency }}</div>
<div>الإجمالي: {{ cartService.total() | currency }}</div>
</div>
<ng-template #emptyCart>
<p>سلة التسوق فارغة</p>
</ng-template>
`
})
export class CartComponent {
constructor(public cartService: CartService) {}
}في هذا المثال، نرى كيف تبسط Angular Signals إدارة حالة سلة التسوق بشكل كبير. بدلاً من استخدام BehaviorSubject والاشتراكات، نستخدم signal واحد لتخزين عناصر السلة، ثم ننشئ computed signals للقيم المشتقة مثل الإجمالي والضرائب. عندما يتغير أي عنصر في السلة، يتم تحديث جميع القيم المشتقة تلقائياً، ويتم تحديث الواجهة فقط للأجزاء المتأثرة بالتغيير. هذا يعني أنه إذا قمت بتحديث كمية منتج واحد، فلن يتم إعادة رسم المكونات الأخرى التي لا تعتمد على هذا المنتج، مما يحسن الأداء بشكل كبير.
لاحظ أيضاً أننا لم نضطر إلى كتابة أي كود لإدارة الاشتراكات أو إلغاءها، لأن Angular Signals تتعامل مع هذا تلقائياً. كما أن الكود أصبح أكثر وضوحاً وقابلية للصيانة، حيث يمكن قراءة تدفق البيانات بسهولة من الـ signal إلى الـ computed signals، وصولاً إلى الواجهة. هذا النهج يقلل بشكل كبير من فرص حدوث الأخطاء، ويجعل من السهل إضافة ميزات جديدة دون القلق بشأن تأثيرها على أداء التطبيق.
رغم أن Angular Signals تقدم حلولاً رائعة للعديد من المشاكل، إلا أنها ليست خالية من التحديات. أول مشكلة قد تواجهها هي التعامل مع الـ Side Effects. في المثال السابق، استخدمنا computed signals لحساب القيم المشتقة، لكن أحياناً قد تحتاج إلى تنفيذ إجراءات جانبية مثل إرسال طلب HTTP عند تغيير حالة معينة. في هذه الحالة، قد تكون الـ effects هي الحل، لكن استخدامها بشكل غير صحيح يمكن أن يؤدي إلى مشاكل مثل الـ Infinite Loops أو الـ Memory Leaks.
خذ مثلاً سيناريو حيث تريد إرسال طلب HTTP لحفظ حالة السلة على السيرفر عند كل تغيير. قد تفكر في استخدام effect لمراقبة تغييرات cartItems وإرسال الطلب. لكن المشكلة هنا أن كل تغيير في cartItems سيؤدي إلى تشغيل الـ effect، مما قد يؤدي إلى إرسال طلبات HTTP كثيرة جداً إذا كان المستخدم يعدل الكمية عدة مرات بسرعة. الحل هنا هو استخدام تقنيات مثل الـ Debouncing أو الـ Throttling، أو حتى أفضل، استخدام آلية حفظ يدوية بدلاً من الاعتماد على الـ effect.
// مثال على استخدام effect مع Debouncing
import { effect } from '@angular/core';
import { debounceTime, Subject } from 'rxjs';
// في خدمة CartService
private saveTrigger = new Subject<void>();
constructor() {
// إعداد effect لمراقبة التغييرات
effect(() => {
this.cartItems(); // مجرد قراءة cartItems لتشغيل الـ effect عند التغيير
this.saveTrigger.next();
});
// إعداد Debouncing لحفظ الحالة
this.saveTrigger.pipe(
debounceTime(1000)
).subscribe(() => {
this.saveCartToServer();
});
}
private saveCartToServer() {
// إرسال حالة السلة إلى السيرفر
}مشكلة أخرى شائعة هي التعامل مع البيانات غير المتزامنة. الـ Signals مصممة للعمل مع القيم المتزامنة، لكن في التطبيقات الحقيقية، غالباً ما نتعامل مع بيانات تأتي من الـ API أو قواعد البيانات. إذا حاولت استخدام signal لتخزين بيانات غير متزامنة، فقد تواجه مشاكل مع القيم الأولية أو حالات التحميل. الحل هنا هو استخدام pattern مثل الـ Loading State مع Signals، حيث تخزن حالة التحميل والخطأ والبيانات في signals منفصلة.
// مثال على التعامل مع البيانات غير المتزامنة باستخدام Signals
import { signal, computed } from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
}
class ProductService {
private products = signal<Product[]>([]);
private loading = signal(false);
private error = signal<string | null>(null);
// computed signal لحالة البيانات
productsState = computed(() => ({
data: this.products(),
loading: this.loading(),
error: this.error()
}));
async fetchProducts() {
this.loading.set(true);
this.error.set(null);
try {
const resp await fetch('https://api.example.com/products');
const data = await response.json();
this.products.set(data);
} catch (err) {
this.error.set('فشل تحميل المنتجات');
} finally {
this.loading.set(false);
}
}
}أخيراً، يجب أن تكون حذراً عند دمج Angular Signals مع RxJS. في التطبيقات الكبيرة، غالباً ما ستجد نفسك بحاجة إلى استخدام الـ Observables مع الـ Signals، خاصة عند التعامل مع الـ HTTP Requests أو الـ WebSockets. المشكلة هنا أن الـ Signals وRxJS لهما نماذج مختلفة للتفاعل، وقد يؤدي الخلط بينهما بشكل غير صحيح إلى مشاكل في الأداء أو سلوك غير متوقع. الحل هو استخدام الأدوات المخصصة مثل toSignal وtoObservable لتحويل البيانات بين النظامين عند الحاجة.
مع ظهور Angular Signals، يتساءل الكثيرون عما إذا كانت هذه هي نهاية RxJS في تطبيقات Angular. الحقيقة هي أن Signals وRxJS يهدفان إلى حل مشكلات مختلفة، وعلى الرغم من أن Signals قد تقلل من اعتمادنا على RxJS في بعض السيناريوهات، إلا أنها لن تحل محلها تماماً. RxJS لا تزال الأداة الأقوى للتعامل مع البيانات غير المتزامنة والمعقدة مثل الـ Streams، الـ WebSockets، والـ Event-Based Systems. أما Signals فهي الأفضل لإدارة الحالة المتزامنة داخل التطبيق، خاصة عندما يتعلق الأمر بتحديث الواجهة بكفاءة.
في رأيي، مستقبل إدارة الحالة في Angular سيكون مزيجاً ذكياً بين Signals وRxJS. ستستخدم Signals لإدارة الحالة المحلية داخل المكونات والخدمات، بينما ستستخدم RxJS للتعامل مع البيانات غير المتزامنة والتفاعلات الخارجية. هذا المزيج سيمنحك أفضل ما في العالمين: الأداء الفائق والـ Fine-Grained Reactivity من Signals، والمرونة والقوة من RxJS. بالفعل، بدأت أرى تطبيقات تستخدم هذا النهج بنجاح، حيث يتم تخزين البيانات الأولية في Signals، ثم تحويلها إلى Observables عند الحاجة للتعامل مع الـ HTTP أو الـ WebSockets.
// مثال على دمج Signals مع RxJS
import { signal, computed } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
class SearchService {
private searchQuery = signal('');
private loading = signal(false);
private results = signal<any[]>([]);
// تحويل signal إلى Observable
searchQuery$ = toObservable(this.searchQuery);
constructor(private http: HttpClient) {
// استخدام RxJS مع Signal
this.searchQuery$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => {
this.loading.set(true);
return this.http.get(`https://api.example.com/search?q=${query}`);
})
).subscribe(data => {
this.results.set(data);
this.loading.set(false);
});
}
// تحديث query من الواجهة
updateQuery(query: string) {
this.searchQuery.set(query);
}
}في هذا المثال، نرى كيف يمكن لـ Signals وRxJS أن يعملا معاً بسلاسة. نستخدم signal لتخزين حالة البحث، ثم نحولها إلى Observable باستخدام toObservable. بعد ذلك، نستخدم RxJS لإضافة سلوكيات مثل الـ Debouncing وswitchMap للتعامل مع طلبات HTTP. هذا النهج يجمع بين بساطة Signals وقوة RxJS، مما يسمح لنا ببناء تطبيقات معقدة بأداء عالٍ وسهولة صيانة.
إذا كنت تعمل على مشروع Angular جديد، فابدأ باستخدام Angular Signals من اليوم الأول. إنها ليست مجرد ميزة جديدة، بل هي تغيير جذري في كيفية إدارة الحالة داخل التطبيقات، وستوفر عليك ساعات من المعاناة مع الـ Change Detection والأداء. ابدأ بتحويل الـ State المحلية داخل المكونات إلى signals، ثم انتقل إلى الخدمات المشتركة. استخدم computed signals للقيم المشتقة، وكن حذراً مع الـ effects لتجنب الـ Side Effects غير المرغوب فيها.
إذا كنت تعمل على مشروع قائم، فلا داعي لإعادة كتابة كل شيء مرة واحدة. ابدأ بتحويل الأجزاء التي تعاني من مشاكل في الأداء أو التعقيد في إدارة الحالة. مثلاً، إذا كان لديك مكونات كثيرة تعتمد على خدمة مشتركة تحتوي على BehaviorSubject، ففكر في تحويل هذه الخدمة لاستخدام Signals بدلاً من ذلك. ستجد أن الكود يصبح أكثر وضوحاً، والأداء يتحسن بشكل ملحوظ، خاصة في التطبيقات الكبيرة والمعقدة.
أخيراً، لا تنسَ أن تتعلم كيف تعمل Angular Signals خلف الكواليس. فهم الـ Effect Graph والـ Glitch-Free Updates سيساعدك على كتابة كود أفضل وتجنب المشاكل الشائعة. تذكر أن Angular Signals ليست مجرد أداة جديدة، بل هي تغيير في طريقة تفكيرك في إدارة الحالة داخل تطبيقات Angular. استغل هذه الفرصة لإعادة تقييم كيفية بناء تطبيقاتك، وابحث دائماً عن طرق لتحسين الأداء وسهولة الصيانة باستخدام هذه التقنية القوية.
في النهاية، Angular Signals ليست مجرد ميزة إضافية في Angular، بل هي ثورة حقيقية في إدارة الحالة. لقد انتظرنا طويلاً أداة تجمع بين بساطة الـ State Management والأداء العالي، والآن أصبحت هذه الأداة حقيقة. إذا كنت تريد بناء تطبيقات Angular سريعة وسهلة الصيانة، فلا تنتظر – ابدأ باستخدام Angular Signals اليوم، وسترى الفرق بنفسك في كل سطر تكتبه.