{"id":"l-rf-3-2","courseId":"react-fundamentals","lang":"ar","kind":"lesson","summary":"اربط الـ event handlers بالطريقة الصحيحة، ومرّر إليها الوسائط، واقرأ كائن الحدث، وتحكّم في انتشار الأحداث والسلوك الافتراضي، ومرّر المعالجات إلى الـ components الأبناء كـ props.","html":"<p>يتغيّر الـ state لأن شيئًا ما يحدث: نقرة، أو ضغطة مفتاح، أو إرسال نموذج. والتعامل مع الأحداث في React قريب من الـ DOM، مع بعض الأعراف التي تُبقي الـ components مقروءة، وفخّين يقع فيهما الجميع مرة على الأقل.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">مرّر الدالة، لا تستدعها</h2>\n<p>تربط event handler (معالج الحدث) بتمرير دالة إلى prop مثل <code dir=\"ltr\">onClick</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ClearButton</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({ onClear }) </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleClick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    onClear</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Clear watched&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>الأقواس المعقوصة تحمل <code dir=\"ltr\">handleClick</code>، أي الدالة نفسها. تحتفظ بها React وتستدعيها حين تقع النقرة. قارن:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Right: passes the function. React calls it on click.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Clear watched&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Wrong: calls it immediately, during render.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">handleClick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Clear watched&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>النسخة الثانية تنفّذ <code dir=\"ltr\">handleClick</code> في كل مرة يُرسم فيها الـ component، وتمرّر قيمتها المُرجعة (<code dir=\"ltr\">undefined</code>) كمعالج. وإذا كانت الدالة تضبط state، فستحصل على حلقة لا نهائية توقفها React بالخطأ «Too many re-renders». حين ترى هذا الخطأ، ابحث عن أقواس استدعاء داخل <code dir=\"ltr\">onSomething={…}</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">تمرير الوسائط</h2>\n<p>أغلب المعالجات في القوائم تحتاج إلى معرفة العنصر الذي نُقر عليه. غلّف الاستدعاء بـ arrow function:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{movies.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">movie</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> key</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onRemove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movie.id)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Remove&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))}</span></span></code></pre></div>\n<p>تُنشأ الـ arrow function أثناء الـ render لكنها لا تُنفَّذ إلا عند النقر، وحين تُنفَّذ تستدعي <code dir=\"ltr\">onRemove</code> بالمعرّف الصحيح. إنشاء دالة صغيرة لكل عنصر أمر طبيعي ورخيص؛ لا تلوِ كودك لتتجنّبه.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">المعالجات كـ props</h2>\n<p>الـ components التي تملك state تمرّر المعالجات إلى الأسفل، ويستدعيها الأبناء ليبلّغوا بما حدث. والأعراف تجعل هذا مقروءًا:</p>\n<ul>\n<li>داخل الـ component، سمِّ المعالجات بالشكل <strong><code dir=\"ltr\">handleSomething</code></strong>: <code dir=\"ltr\">handleRemove</code> و<code dir=\"ltr\">handleSubmit</code>.</li>\n<li>والـ props التي تستقبل المعالجات تُسمّى بالشكل <strong><code dir=\"ltr\">onSomething</code></strong>: <code dir=\"ltr\">onRemove</code> و<code dir=\"ltr\">onToggle</code>، تماشيًا مع الأحداث المدمجة مثل <code dir=\"ltr\">onClick</code>.</li>\n</ul>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/App.jsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> App</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">movies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">INITIAL_MOVIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleRemove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movies.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">m</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">MovieList</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> movies</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movies</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onRemove</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleRemove</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>لا يعرف <code dir=\"ltr\">MovieList</code> شيئًا عن طريقة الحذف. كل ما يعرفه أنه حين ينقر المستخدم Remove، يستدعي <code dir=\"ltr\">onRemove(id)</code>. ويبقى الـ state، وقواعد تغييره، في مكان واحد.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">كائن الحدث</h2>\n<p>تستدعي React معالجك مع كائن حدث. إنه يغلّف الحدث الأصلي في المتصفّح، وله الواجهة المألوفة نفسها في كل متصفّح:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleKeyDown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (e.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Escape'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setSelectedId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onKeyDown</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleKeyDown</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>الخصائص التي ستستخدمها أكثر من غيرها هي <code dir=\"ltr\">e.target</code> (العنصر الذي بدأ منه الحدث)، و<code dir=\"ltr\">e.currentTarget</code> (العنصر الذي يعمل معالجه الآن)، و<code dir=\"ltr\">e.key</code> لأحداث لوحة المفاتيح، ودالتان: <code dir=\"ltr\">e.preventDefault()</code> و<code dir=\"ltr\">e.stopPropagation()</code>.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">الانتشار والسلوك الافتراضي</h2>\n<p>الأحداث <strong>تنتشر صعودًا (bubbling)</strong>: النقرة على زر داخل بطاقة تُطلق معالج الزر، ثم معالج البطاقة، ثم معالج أي سلف. ولهذا يلتقط <code dir=\"ltr\">onKeyDown</code> الموجود على <code dir=\"ltr\">&lt;main&gt;</code> أعلاه ضغطة Escape حين يكون التركيز على أي زر بداخله. في الغالب يكون الانتشار ما تريده، لكن ليس دائمًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onOpen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movie.id)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      e.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stopPropagation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">      onRemove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movie.id);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    Remove</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>من دون <code dir=\"ltr\">stopPropagation</code>، سيفتح النقر على Remove تفاصيل الفيلم أيضًا. (جعل البطاقة كلها هدفًا للنقر راحة لمستخدمي الفأرة؛ فاحتفظ بزر أو رابط حقيقي لفتح التفاصيل أيضًا، حتى يصل إليها مستخدمو لوحة المفاتيح.) احتفظ بهذا النمط للحالات التي تحتاجه؛ فاللجوء إليه في كل مكان يجعل تدفّق الأحداث صعب المتابعة.</p>\n<p>أمّا <code dir=\"ltr\">preventDefault</code> فمختلفة: إنها توقف السلوك المدمج في <strong>المتصفّح</strong>، مثل انتقال رابط، أو إعادة تحميل الصفحة عند إرسال نموذج. ولا توقف الانتشار. ستستخدمها في نموذج Watchlist في الدرس التالي.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الـ divs القابلة للنقر</span></p>\n<p>الكود <code dir=\"ltr\">&lt;div onClick={select}&gt;Arrival&lt;/div&gt;</code> يعمل بالفأرة ولا شيء غيرها. لا يستقبل التركيز من لوحة المفاتيح، وEnter وSpace لا يفعلان شيئًا، والـ screen readers لا تعلنه كعنصر تفاعلي. استخدم <code dir=\"ltr\">&lt;button type=&quot;button&quot;&gt;</code> للإجراءات و<code dir=\"ltr\">&lt;a href&gt;</code> للتنقّل. ويمكنك تنسيق الزر ليبدو كما تشاء.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">المعالجات هي مكان الآثار الجانبية</h2>\n<p>يجب أن تكون الـ components نقية أثناء الـ render، لكن معالجات الأحداث لا تعمل أثناء الـ render. إنها تعمل لأن المستخدم فعل شيئًا، فهي المكان الصحيح للآثار الجانبية (side effects): ضبط الـ state، والكتابة في localStorage، وإرسال طلب، وتسجيل التحليلات. وحين لا تكون متأكّدًا أين يجب أن يعيش كود ما، اسأل: «ما الذي تسبّب فيه؟» إذا كان الجواب إجراءً محدّدًا من المستخدم، فمكانه معالج ذلك الإجراء.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>اقرأ الخطأ، واعثر على الأقواس</span></p>\n<p>الخطأ «Too many re-renders» يعني في الغالب أن setter يُستدعى أثناء الـ render، وعادةً عبر <code dir=\"ltr\">onClick={doSomething()}</code>. والمعالج الذي لا يفعل شيئًا بصمت، أو يطبع كائن حدث حيث كنت تتوقّع معرّفًا، مُرِّر غالبًا مباشرةً (<code dir=\"ltr\">onClick={onRemove}</code>) بدل أن يُغلَّف بـ arrow function. وكلاهما ينشأ من طريقة تمرير الدالة.</p>\n</aside>\n<p>في الدرس التالي تتعامل مع أهم حدث في Watchlist، وهو إرسال النموذج الذي يضيف فيلمًا، وتتعلّم كيف تُبقي الـ controlled inputs حقول النموذج والـ state متزامنين.</p>\n","toc":[{"id":"sec-1","text":"مرّر الدالة، لا تستدعها","level":2},{"id":"sec-2","text":"تمرير الوسائط","level":2},{"id":"sec-3","text":"المعالجات كـ props","level":2},{"id":"sec-4","text":"كائن الحدث","level":2},{"id":"sec-5","text":"الانتشار والسلوك الافتراضي","level":2},{"id":"sec-6","text":"المعالجات هي مكان الآثار الجانبية","level":2}],"takeaways":["مرّر دالة إلى <code dir=\"ltr\">onClick</code>، مثل <code dir=\"ltr\">onClick={handleClick}</code>؛ أمّا كتابة <code dir=\"ltr\">handleClick()</code> فتستدعيها أثناء الـ render.","لتمرير وسيط، غلّف الاستدعاء بـ arrow function، مثل <code dir=\"ltr\">onClick={() =&gt; onRemove(movie.id)}</code>.","تستقبل المعالجات كائن حدث فيه <code dir=\"ltr\">target</code> و<code dir=\"ltr\">key</code> و<code dir=\"ltr\">preventDefault()</code> و<code dir=\"ltr\">stopPropagation()</code>.","سمِّ المعالجات داخل الـ component بالشكل <code dir=\"ltr\">handleX</code>، وسمِّ الـ callback props بالشكل <code dir=\"ltr\">onX</code>، ليُقرأ تدفّق البيانات بوضوح.","استخدم عناصر <code dir=\"ltr\">&lt;button&gt;</code> حقيقية للأشياء القابلة للنقر، حتى يستطيع مستخدمو لوحة المفاتيح والـ screen reader استخدامها أيضًا."],"further":[{"title":"Responding to Events","url":"https://react.dev/learn/responding-to-events"},{"title":"Common components (React event object)","url":"https://react.dev/reference/react-dom/components/common#react-event-object"},{"title":"Event bubbling (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"}],"quiz":[{"q":"<p>الكود <code dir=\"ltr\">&lt;button onClick={setSelectedId(movie.id)}&gt;Select&lt;/button&gt;</code> يُسقط التطبيق بالخطأ «Too many re-renders». لماذا؟</p>","options":[{"text":"يُستدعى الـ setter أثناء الـ render، فيُطلق render آخر، يستدعيه مجددًا، إلى ما لا نهاية.","why":"صحيح. الأقواس المعقوصة تحتوي على استدعاء، فيُنفَّذ أثناء الـ render. اكتب <code dir=\"ltr\">onClick={() =&gt; setSelectedId(movie.id)}</code> لتمرّر دالة بدلًا من ذلك."},{"text":"لا تقبل <code dir=\"ltr\">onClick</code> إلا الدوال المعرّفة بالكلمة <code dir=\"ltr\">function</code>.","why":"أي دالة تعمل، بما فيها الـ arrow functions. المشكلة أن هذا ليس دالة أصلًا؛ إنه نتيجة استدعاء."},{"text":"الأزرار لا تستطيع تغيير الـ state؛ النماذج وحدها تستطيع.","why":"أي event handler يستطيع ضبط الـ state. ونقرة الزر هي الطريقة الأكثر شيوعًا لذلك."}],"answer":0},{"q":"<p>يستقبل <code dir=\"ltr\">MovieItem</code> الـ prop المسمّى <code dir=\"ltr\">onRemove</code> من أبيه. والدالة <code dir=\"ltr\">removeMovie(id)</code> في الأب تتوقّع معرّف فيلم. أيّ سطر صحيح داخل MovieItem؟</p>","options":[{"text":"<code dir=\"ltr\">&lt;button onClick={onRemove}&gt;Remove&lt;/button&gt;</code>","why":"تستدعي React الدالة <code dir=\"ltr\">onRemove</code> مع حدث النقر، فيستقبل الأب كائن حدث بدل المعرّف."},{"text":"<code dir=\"ltr\">&lt;button onClick={onRemove(movie.id)}&gt;Remove&lt;/button&gt;</code>","why":"هذا يستدعي <code dir=\"ltr\">onRemove</code> أثناء الـ render ويحذف الفيلم فورًا، قبل أن ينقر أحد."},{"text":"<code dir=\"ltr\">&lt;button onClick={() =&gt; onRemove(movie.id)}&gt;Remove&lt;/button&gt;</code>","why":"صحيح. تُنفَّذ الـ arrow function عند النقر وتستدعي <code dir=\"ltr\">onRemove</code> بالوسيط الذي يتوقّعه الأب بالضبط."},{"text":"<code dir=\"ltr\">&lt;button onRemove={movie.id}&gt;Remove&lt;/button&gt;</code>","why":"عناصر الـ DOM لا تعرف شيئًا عن <code dir=\"ltr\">onRemove</code>. وحدها الأحداث الحقيقية مثل <code dir=\"ltr\">onClick</code> تُربط."}],"answer":2},{"q":"<p>لبطاقة فيلم <code dir=\"ltr\">onClick={openDetails}</code>، وبداخلها زر Remove له <code dir=\"ltr\">onClick={() =&gt; onRemove(movie.id)}</code>. النقر على Remove يفتح التفاصيل أيضًا. ما الإصلاح؟</p>","options":[{"text":"استدعِ <code dir=\"ltr\">e.preventDefault()</code> في معالج Remove.","why":"توقف preventDefault الإجراء الافتراضي للمتصفّح، مثل إرسال نموذج. لكنها لا توقف انتشار النقرة صعودًا إلى البطاقة."},{"text":"استدعِ <code dir=\"ltr\">e.stopPropagation()</code> في معالج Remove قبل استدعاء <code dir=\"ltr\">onRemove</code>.","why":"صحيح. تنتشر النقرة من الزر إلى البطاقة، وstopPropagation توقفها عند الزر."},{"text":"انقل زر Remove إلى خارج الـ component.","why":"هذا ينجح بصريًا فقط إن سمح التخطيط بذلك، وهو يتجنّب المشكلة بدل أن يفهمها."},{"text":"استخدم <code dir=\"ltr\">onMouseDown</code> على زر Remove بدلًا من ذلك.","why":"يظل معالج النقر في البطاقة يعمل عند النقر، والأحداث الخاصة بالفأرة وحده تكسر الاستخدام بلوحة المفاتيح."}],"answer":1},{"q":"<p>لماذا يجب أن يكون عنوان الفيلم القابل للنقر <code dir=\"ltr\">&lt;button&gt;</code> بدلًا من <code dir=\"ltr\">&lt;div onClick={…}&gt;</code>؟</p>","options":[{"text":"الـ onClick على div لا يعمل إلا في React 18 وما قبلها.","why":"يعمل onClick على أي عنصر في كل إصدارات React. المشكلة فيما يقدّمه العنصر للمستخدمين."},{"text":"الأزرار تُرسم أسرع من الـ divs.","why":"سرعة الرسم متساوية. الفرق في السلوك المدمج وإمكانية الوصول."},{"text":"تحذّر React من معالجات النقر على الـ divs وترفض تشغيلها.","why":"لا ترفض React؛ النقرة تعمل بالفأرة. مستخدمو لوحة المفاتيح والتقنيات المساعدة هم من يُتركون خارجًا."},{"text":"الزر يقبل التركيز، ويعمل مع Enter وSpace، ويعلنه الـ screen reader كزر؛ والـ div لا يفعل شيئًا من ذلك.","why":"صحيح. ستضطر إلى إضافة role وtabIndex ومعالجات مفاتيح إلى الـ div لتحصل على ما يعطيك إياه الزر مجانًا."}],"answer":3}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useState } from 'react';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival' },\n  { id: 'p2', title: 'Past Lives' },\n  { id: 'd3', title: 'Dune: Part Two' },\n];\n\nfunction MovieItem({ movie, onSelect, onRemove }) {\n  return (\n    <li>\n      <button className=\"title\" onClick={() => onSelect(movie.id)}>\n        {movie.title}\n      </button>\n      <button className=\"remove\" onClick={onRemove}>\n        Remove\n      </button>\n    </li>\n  );\n}\n\nexport default function App() {\n  const [movies, setMovies] = useState(INITIAL);\n  const [selectedId, setSelectedId] = useState(null);\n  const selected = movies.find((m) => m.id === selectedId);\n\n  function handleRemove(id) {\n    setMovies(movies.filter((m) => m.id !== id));\n  }\n\n  // Clear the selection when Escape is pressed anywhere inside <main>\n\n  return (\n    <main>\n      <p id=\"selected\">Selected: {selected ? selected.title : 'none'}</p>\n      <ul>\n        {movies.map((movie) => (\n          <MovieItem\n            key={movie.id}\n            movie={movie}\n            onSelect={setSelectedId}\n            onRemove={handleRemove}\n          />\n        ))}\n      </ul>\n    </main>\n  );\n}\n"},"solution":{"jsx":"import { useState } from 'react';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival' },\n  { id: 'p2', title: 'Past Lives' },\n  { id: 'd3', title: 'Dune: Part Two' },\n];\n\nfunction MovieItem({ movie, onSelect, onRemove }) {\n  return (\n    <li>\n      <button className=\"title\" onClick={() => onSelect(movie.id)}>\n        {movie.title}\n      </button>\n      <button className=\"remove\" onClick={() => onRemove(movie.id)}>\n        Remove\n      </button>\n    </li>\n  );\n}\n\nexport default function App() {\n  const [movies, setMovies] = useState(INITIAL);\n  const [selectedId, setSelectedId] = useState(null);\n  const selected = movies.find((m) => m.id === selectedId);\n\n  function handleRemove(id) {\n    setMovies(movies.filter((m) => m.id !== id));\n  }\n\n  function handleKeyDown(e) {\n    if (e.key === 'Escape') {\n      setSelectedId(null);\n    }\n  }\n\n  return (\n    <main onKeyDown={handleKeyDown}>\n      <p id=\"selected\">Selected: {selected ? selected.title : 'none'}</p>\n      <ul>\n        {movies.map((movie) => (\n          <MovieItem\n            key={movie.id}\n            movie={movie}\n            onSelect={setSelectedId}\n            onRemove={handleRemove}\n          />\n        ))}\n      </ul>\n    </main>\n  );\n}\n"},"checks":[{"id":"select","test":"const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); await click(row('Past Lives').querySelector('.title')); return text('#selected') === 'Selected: Past Lives';"},{"id":"remove","test":"const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); await click(row('Arrival').querySelector('.remove')); return $$('li').length === 2 && !row('Arrival') && text('#selected') === 'Selected: Past Lives';"},{"id":"remove-selected","test":"const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); await click(row('Past Lives').querySelector('.remove')); return $$('li').length === 1 && text('#selected') === 'Selected: none';"},{"id":"escape","test":"const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); await click(row('Dune: Part Two').querySelector('.title')); if (text('#selected') !== 'Selected: Dune: Part Two') return false; await press(row('Dune: Part Two').querySelector('.title'), 'Escape'); return text('#selected') === 'Selected: none';"}],"title":"التحديد والحذف وEscape","prompt":"<p>النقر على عنوان فيلم يحدّده. لكن شيئين ناقصان:</p>\n<ol>\n<li>زر <strong>Remove</strong> لا يحذف شيئًا. الدالة <code dir=\"ltr\">handleRemove(id)</code> في <code dir=\"ltr\">App</code> تتوقّع معرّف فيلم؛ أصلح طريقة استدعاء <code dir=\"ltr\">MovieItem</code> للدالة <code dir=\"ltr\">onRemove</code>.</li>\n<li>الضغط على <strong>Escape</strong> بينما التركيز في أي مكان داخل <code dir=\"ltr\">&lt;main&gt;</code> يجب أن يلغي التحديد، فيصبح نص <code dir=\"ltr\">#selected</code> هو <strong>Selected: none</strong>.</li>\n</ol>\n<p>وحين يُحذف الفيلم المحدَّد، يجب أن يعرض التحديد <strong>none</strong> أيضًا. (انظر إلى طريقة حساب <code dir=\"ltr\">selected</code> قبل أن تضيف أي كود لهذا.)</p>\n","hints":["حاليًا يعني <code dir=\"ltr\">onClick={onRemove}</code> أن React تستدعي <code dir=\"ltr\">onRemove(event)</code>. غلّفه بـ arrow function لتمرّر <code dir=\"ltr\">movie.id</code>.","أحداث لوحة المفاتيح تنتشر صعودًا. معالج <code dir=\"ltr\">onKeyDown</code> واحد على <code dir=\"ltr\">&lt;main&gt;</code> يرى Escape المضغوط على أي زر بداخله.","في المعالج، تحقّق من <code dir=\"ltr\">e.key === 'Escape'</code> واستدعِ <code dir=\"ltr\">setSelectedId(null)</code>."],"explanation":"<p>الكود <code dir=\"ltr\">onClick={onRemove}</code> يسلّم React دالة تستدعيها مع حدث النقر، فاستقبلت <code dir=\"ltr\">handleRemove</code> حدثًا بدل المعرّف ولم تُصفِّ شيئًا. أمّا <code dir=\"ltr\">() =&gt; onRemove(movie.id)</code> فتمرّر بالضبط الوسيط الذي يتوقّعه الأب. ويعمل Escape من معالج واحد على <code dir=\"ltr\">&lt;main&gt;</code> لأن أحداث keydown تنتشر صعودًا من الزر الذي عليه التركيز. وحذف الفيلم المحدَّد لا يحتاج إلى كود إضافي: <code dir=\"ltr\">selected</code> مشتقّة باستخدام <code dir=\"ltr\">find</code>، فبمجرّد اختفاء الفيلم تصبح قيمتها <code dir=\"ltr\">undefined</code> ويرجع النص إلى “none”.</p>\n","checkLabels":{"select":"النقر على “Past Lives” يحدّده","remove":"زر Remove يحذف Arrival ويُبقي التحديد","remove-selected":"حذف الفيلم المحدَّد يعيد التحديد إلى none","escape":"زر Escape يلغي التحديد"}},"words":616,"readMinutes":4}