Skip to content

Latest commit

 

History

History
866 lines (561 loc) · 29.1 KB

File metadata and controls

866 lines (561 loc) · 29.1 KB

When (Not) to use Effects in Angular — and what to do instead

⬆️ Back to Top

إمتى تستخدم effects في Angular — وإمتى ما تستخدمهاش وإيه البدائل؟

الEffects أداة قوية في Angular، لكن مش لازم تستخدمها في كل الحالات.

في حالات معينة، استخدام effects بيكون مناسب، وفي حالات تانية ممكن يسبب تعقيد بدون فائدة حقيقية.

خلينا نستعرض إمتى نستخدمها وإمتى لأ، وإيه البدائل المتاحة.

الEffects مفيدة بشكل أساسي للتعامل مع الحاجات اللي مش بتقدر تعرضها باستخدام (data binding).

في Angular، ربط البيانات (data binding) هو الطريقة اللي تقدر بيها تعرض أو تربط البيانات بالواجهة (UI) بسهولة، زي لما تظهر قيمة في مكان معين أو تربط زرار بوظيفة. لكن في بعض الحالات، ربط البيانات ما بيكونش كافي أو ما ينفعش أصلاً بسبب احتياجك لتفاعل مباشر أكتر.

أمثلة للاستخدامات الرئيسية لـEffects

1) Logging

2) Painting on a Canvas

3) Custom DOM behavior

Keep Auto-Tracking in Mind

في Angular، فيه حاجة اسمها (auto-tracking)، ودي شغالة مع computed وeffects. يعني Angular بتراقب أي قيمة (Signal) حتى لو مش مستخدمة بشكل مباشر في الكود، لكن طالما فيه تأثير عليها بشكل غير مباشر.

مثال للتوضيح

error = signal(null); // تبدأ بقيمة null أو undefined

effect(() => {
  this.logError();
});

logError(): void {
  const error = this.error();
  if (error) {
    console.error(error);
  }
}

هنا عندنا effect بيشغل logError، وداخل logError بنستخدم قيمة error. مع إن error مش موجودة مباشرة في effect،

الAngular عارفة إن فيه علاقة غير مباشرة بينها وبين effect. فلو حصل تغيير في error، effect هيشتغل تاني تلقائي.

ليه الموضوع ده مهم؟

التصميم ده بيأكد إن Effects في Angular معمولة بشكل أساسي للتعامل مع عرض البيانات (rendering).

أي قيمة بيتم التعامل معاها أثناء العرض، Angular بتراقبها تلقائي، ولو حصل أي تغيير فيها، بيتم تحديث العرض مباشرة.

Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren

⬆️ Back to Top

مع ظهور الـ Signals في Angular، بقى عندنا دلوقتي طريقة جديدة تماماً لكتابة الـ Components في Angular باستخدام الـ Signal-based APIs، ومن غير الحاجة للـ Decorators.

جزء كبير من الطريقة الجديدة دي في كتابة الـ Components هو إدخال حاجة جديدة اسمها Signal-based Template Queries

viewChild()

contentChild()

viewChildren()

contentChildren()

الـ APIs دي بقت بديل جديد عن الـ Decorator-based Template Queries التقليدية زي ViewChild@، ContentChild@، ViewChildren@، و ContentChildren@.

🔴 بيشتغلوا بنفس الطريقة بالظبط، لكن:

بقى نظامهم Signal-based

أسهل في الفهم والاستخدام

سهل تدمجهم مع أي Signals تانية

وفي العادي مش محتاجين تستخدم معاهم الـ Lifecycle Hooks

هنتعمق في التفاصيل بتاعت الـ Signal Queries دي، وهنستكشف الـ Syntax بتاعهم، طرق استخدامهم، والمميزات اللي بتخليهم أحسن من الـ Traditional Decorator-based Queries.

What is viewChild()?

الviewChild ده أسلوب بنستخدمه عشان نوصل لعناصر موجودة جوه تصميم (template) الكومبوننت بتاعنا.

العناصر دي ممكن تكون يا إما أجزاء (components) تانية معمولالها design في Angular أو عناصر HTML عادية زي <div> و <button>.

الـ viewChild بيعمل نفس وظيفة الـ ViewChild decorator، يعني الاتنين بيساعدونا نوصل للعناصر دي ونتعامل معاها.

هنبدأ الأول إزاي نوصل لعناصر الـ HTML العادية، وبعد كده هنشوف إزاي نوصل للكومبوننتات (components)

Querying plain HTML elements with viewChild()

عشان نجيب عنصر من الـtemplate، لازم نديله "template reference" باستخدام علامة #.

ده مثال على الطريقة:

@Component({
  selector: "book",
  template: `
    <div>
      <b #title>Title</b>
    </div>
  `,
})
class BookComponent {
  title = viewChild<ElementRef>("title");

  constructor() {
    effect(() => {
      console.log("Title: ", this.title()?.nativeElement);
    });
  }
}

زي ما انت شايف، استخدمنا title # كـ template reference عشان نوصل للعنصر الـ HTML نفسه.

وبعدين استخدمنا viewChild عشان نجيب العنصر اللي حطينا عليه الـ reference ده.

الـ viewChild بيرجع لنا "signal" بتحتوي على العنصر اللي جبناه، بس بيكون جوه ElementRef.

عشان نوصل للنتيجة اللي رجعته الـ query، كل اللي علينا إننا نتابع "الsignal" دي باستخدام أي API بيتعامل مع الإشارات زي ()effect أو ()computed.

What is the value returned by this signal?

لاحظ إن عشان نوصل للعنصر الـ HTML الفعلي، لازم نستخدم خاصية nativeElement من قيمة الـ signal.

ده لأن ElementRef هو مجرد غلاف حوالين عنصر الـ DOM الحقيقي، مش العنصر نفسه.

Why did we use a generic parameter ElementRef?

خد بالك كمان إننا استخدمنا الـ <ElementRef> في viewChild، عشان نحدد نوع القيم اللي الـ signal هترجعها.

لو ماعملناش كده، الـ signal هتتعامل على إنها بترجع قيم من نوع unknown، وده مش اللي احنا عايزينه.

وبكده تكون عرفت إزاي تعمل query لعناصر HTML العادية باستخدام ()viewChild بكل بساطة!

What about AfterViewInit?

لاحظ إننا مش محتاجين نستخدم الـ AfterViewInit lifecycle hook التقليدي زي ما كنا بنعمل مع الـ ViewChild decorator.

بدل كده، استخدمنا ()effect كـ signal بسيطة عشان نستقبل إشعار لما عنصر title يبقى جاهز.

وكمان ممكن نستخدم ()computed عشان نستخرج قيم من الـ title signal.

الـ title signal هي مجرد signal للقراءة فقط (read-only)، فده بيخليها سهلة في التعامل وبتندمج بشكل سلس مع باقي الأدوات المبنية على signals في الـ framework.

ده معناه إننا بشكل عام مش هنحتاج نستخدم AfterViewInit تاني لما نشتغل باستخدام query signals بدل الـ decorator التقليدي.

What happens if the value of a template variable occurs more than once?

<div>
  <b #title>First Title</b>
  <b #title>Second Title</b>
</div>

<p #title>Paragraph Title</p>

في الحالة دي، viewChild هيختار أول عنصر موجود فيه title#، ومش هيظهر أي خطأ.

ده يعني إنه لما يكون في أكتر من عنصر ليهم نفس الـ template reference (زي title# هنا)، viewChild دايمًا هياخد أول واحد بس ويهمله الباقي.

viewChild() and Component Queries

بجانب عناصر HTML العادية، نقدر كمان نجيب الـ component instances باستخدام ميزة viewChild.

ممكن نعمل query للـ components إما باستخدام template references زي ما عملنا قبل كده، أو باستخدام كلاس الـ component نفسه.

خلينا نبدأ بعمل query للـ components باستخدام template references:

@Component({
  template: `
    <div>
      <book #book></book>
    </div>
  `,
})
class BookListComponent {
  bookComponent = viewChild<BookComponent>("book");
}

زي ما شايف، إحنا هنا بنستخدم component في الـ template، وحطينا ليه template reference باسم book.

لو مررنا الـ reference ده لـ viewChild، الـ query signal هترجع لنا الـ instance بتاع الـ BookComponent نفسه، مش عنصر الـ HTML المرتبط بيه.

✨ده معناه إننا نقدر نتعامل مع الـ component instance مباشرة، زي كده:

this.bookComponent().title;
this.bookComponent().hello();

خد بالك من استخدام الـ generic parameter BookComponent . البراميتر ده مهم عشان يعرف viewChild نوع الـ component اللي متوقعه من الـ query دي.

وبكده تكون عرفت إزاي تستخدم viewChild عشان تجيب الـ component instances بشكل مباشر، بكل بساطة!

How does viewChild() work if the template changes?

الـ viewChild signal query بيعمل الـ query أول مرة لما الـ view بيتم تهيئته (initialize).

لو في أي وقت العنصر اللي جبناه اتحذف أو اتعمله إعادة عرض (re-rendered) أو اتحدّث في شجرة الكومبوننت، الـ signal query هتعيد الـ query عشان تحدث نفسها بالحالة الحالية للعنصر.

لو العنصر اتحذف، قيمة الـ signal query هتبقى undefined.

لكن لما العنصر يتخلق تاني، الـ signal query هترجع تجيب العنصر من الـ template view مرة تانية.

يعني زي ما شايف، الـ signal query بتحدّث نفسها تلقائيًا في كل مرة يحصل فيها تغيير في الـ detection run.

Setting "read" on viewChild()

الـ viewChild بتشتغل بشكل افتراضي إنها بترجع:

العنصر HTML عادي مغلف في ElementRef لو اللي بنسأل عنه هو عنصر HTML. الكومبوننت نفسه لو العنصر ده هو كومبوننت.

لكن في بعض الحالات ممكن نحتاج نغير السلوك ده عشان نرجع حاجة معينة مرتبطة بالعنصر ده، سواء كانت الكومبوننت نفسه أو حاجات مرتبطة بيه.

مثال 1: الوصول للعنصر الـ HTML بدل الكومبوننت

لو إحنا عندنا كومبوننت اسمه <book/> في الـ template، ومش عايزين نوصل للكومبوننت نفسه، لكن عايزين نوصل للعنصر الـ HTML اللي بيحتويه.

@Component({
  template: `
    <div>
      <book #book></book>
    </div>
  `,
})
class BookListComponent {
  bookComponent = viewChild("book", {
    read: ElementRef,
  });
}

هنا استخدمنا read: ElementRef مع viewChild. ده بيقول لـ viewChild إنها ترجع ElementRef اللي بيحتوي على العنصر HTML نفسه، مش الـ BookComponent.

مثال 2: الوصول لDirective معينة مرتبطة بالعنصر

ممكن يكون عندنا Directive معينة مضافة للعنصر، زي matTooltip اللي بتضيف tooltip على العنصر.

@Component({
  template: `
    <div>
      <book #book matTooltip="I'm a tooltip!"> </book>
    </div>
  `,
  imports: [MatTooltip],
})
class BookListComponent {
  bookComponent = viewChild("book", {
    read: MatTooltip,
  });

  constructor() {
    effect(() => {
      console.log("Tooltip: ", this.bookComponent()?.message);
    });
  }
}

لو عايزين نوصل لحاجة معينة مرتبطة بالعنصر (زي الـ HTML element نفسه أو Directive معينة)، ممكن نحدد النوع اللي محتاجينه باستخدام read مع viewChild.

Making viewChild() to be required

بشكل افتراضي، ()viewChild هترجع undefined لو العنصر اللي بنسأل عنه مش موجود في الـ template view.

لكن ممكن نخلي ()viewChild تعمل query تكون "إجبارية"، بمعنى إنها لازم تلاقي حاجة تطابق الـ query في الـ template. لو مفيش حاجة تطابق، هيظهر خطأ.

<div>
  <b #title>Title</b>
</div>
titleRef = viewChild.required("bold");

في المثال ده، الـ query بتدور على عنصر بالـ reference bold، لكن مفيش عنصر بالـ reference ده، وبالتالي هيظهر الخطأ التالي:

ERROR Error: NG0951: Child query result is required but no value is available