«Expression has changed after it was checked» — لماذا يحدث هذا وكيف تتعايش معه؟
\n\nإذا كنت قد كتبت أي تطبيق باستخدام Angular، فبالتأكيد واجهت هذه الرسالة المخيفة في وحدة التحكم:
\n\nERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.\n\nهذا الخطأ هو "آفة" حقيقية للمطورين. آلاف الأسئلة على Stack Overflow، ومئات الساعات من التصحيح، وعشرات الانهيارات العصبية. لكن الخبر السار: لا يمكنك إصلاحه فحسب، بل يمكنك أيضًا فهم سبب حدوثه. في هذه المقالة، سنحلل أهم أسباب الخطأ Expression has changed after it was checked، ونكتب أمثلة بلغة TypeScript ونقدم حلولاً محددة. هيا بنا!
\n\nماذا يعني هذا الخطأ بالضبط؟
\n\nيعمل Angular في وضعين: التطوير (dev mode) والإنتاج. في وضع التطوير، يقوم الإطار بفحص إضافي — بعد كل دورة كشف تغييرات، يقوم بتشغيل دورة ثانية للتأكد من أن البيانات لم تتغير "أثناء التنفيذ". إذا تغيرت القيمة بين الدورة الأولى والثانية، يتوقف Angular مع الخطأ ExpressionChangedAfterItHasBeenCheckedError.
\n\nبكلمات بسيطة: أخبرت Angular "هذه هي البيانات"، فقام بفحصها، ثم قمت بتغييرها. يغضب Angular ويصرخ.
\n\nأهم 3 أسباب للخطأ
\n\n1. تغيير البيانات في خطافات دورة الحياة بعد ngAfterViewInit
\n\nالسبب الأكثر شيوعًا هو محاولة تغيير خاصية المكون في الخطاف ngAfterViewInit أو ngAfterContentInit أو ngAfterViewChecked. في هذه اللحظة، يكون Angular قد أكمل فحص العرض، وأي تغيير يسبب الخطأ.
مثال بلغة TypeScript:
\n\nimport { Component, AfterViewInit } from '@angular/core';\n\n@Component({\n selector: 'app-example',\n template: '<p>{{ message }}</p>'\n})\nexport class ExampleComponent implements AfterViewInit {\n message = 'قيمة أولية';\n\n ngAfterViewInit(): void {\n // خطأ! تغيير الخاصية بعد فحص العرض\n this.message = 'قيمة معدلة';\n }\n}\n\nهنا، يقوم Angular أولاً بفحص message (القيمة "قيمة أولية")، ثم يستدعي ngAfterViewInit، حيث تقوم بتغييرها إلى "قيمة معدلة". عند الفحص الثاني، يرى Angular اختلافًا ويطرح الخطأ.
2. العمليات غير المتزامنة داخل دورة الحياة
\n\nإذا قمت بتشغيل كود غير متزامن (setTimeout، Promise، Observable) داخل الخطاف، فإن تحديث البيانات يحدث خارج منطقة سيطرة Angular.
\n\nمثال:
\n\nimport { Component, OnInit } from '@angular/core';\n\n@Component({\n selector: 'app-async',\n template: '<p>{{ data }}</p>'\n})\nexport class AsyncComponent implements OnInit {\n data = 'بداية';\n\n ngOnInit(): void {\n setTimeout(() => {\n this.data = 'قيمة جديدة';\n }, 0);\n }\n}\n\nعلى الرغم من أن setTimeout بتأخير 0 مللي ثانية يتم تنفيذه فورًا تقريبًا، إلا أن Angular قد أكمل الفحص بالفعل. في وضع التطوير، سيؤدي هذا إلى خطأ.
\n\n3. المكونات المتداخلة ونقل البيانات عبر Input
\n\nعندما ينقل المكون الأصلي بيانات إلى المكون الفرعي عبر @Input، ويحاول المكون الفرعي تغيير هذه البيانات في دورة حياته، يحدث تعارض.
مثال:
\n\n// المكون الأصلي\n@Component({\n selector: 'app-parent',\n template: '<app-child [value]="parentValue"></app-child>'\n})\nexport class ParentComponent {\n parentValue = 'أصلي';\n}\n\n// المكون الفرعي\n@Component({\n selector: 'app-child',\n template: '<p>{{ value }}</p>'\n})\nexport class ChildComponent implements AfterViewInit {\n @Input() value: string = '';\n\n ngAfterViewInit(): void {\n this.value = 'معدل في'