{"id":"l-rf-3-5","courseId":"react-fundamentals","lang":"ar","kind":"lesson","summary":"استخدم useEffect لإبقاء الـ state في React متزامنًا مع أنظمة خارجها، واكتب الاعتماديات ودوال التنظيف بشكل صحيح، وتعرّف على الحالات الكثيرة التي لا تحتاج فيها إلى effect أصلًا.","html":"<p>حتى الآن، كان كل ما في الـ components متعلّقًا بـ React: props تدخل، وJSX يخرج، وتغييرات الـ state تُطلق الـ render. لكن Watchlist يحتاج أيضًا إلى لمس أشياء لا تديرها React. يجب أن يقول تبويب المتصفّح «2 to watch»، ويجب أن تنجو القائمة من تحديث الصفحة بحفظها في localStorage. لا شيء من هذا رسم. إنه <strong>تزامن (synchronization)</strong>: إبقاء شيء خارج React متوافقًا مع الـ state الخاص بك. وهذه وظيفة الـ effects.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">أول effect لك</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useEffect, useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</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\">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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> left</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</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:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m.watched).</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</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\">    document.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">left</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} to watch · Watchlist`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [left]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …render the app</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تأخذ <code dir=\"ltr\">useEffect</code> دالة و<strong>مصفوفة اعتماديات (dependency array)</strong>. ترسم React الـ component، وتحدّث الشاشة، ثم تشغّل الدالة. وبعد مرّات الـ render اللاحقة، لا تشغّلها مجددًا إلا إذا تغيّر شيء في مصفوفة الاعتماديات منذ المرة السابقة. علّم فيلمًا كمُشاهَد، فتنتقل <code dir=\"ltr\">left</code> من 2 إلى 1، ويتحدّث العنوان. اكتب في نموذج الإضافة، فلا تتغيّر <code dir=\"ltr\">left</code>، ويُتخطّى الـ effect.</p>\n<p>لمصفوفة الاعتماديات ثلاثة أشكال:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>ما تكتبه</th>\n<th>متى يعمل الـ effect</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn, [a, b])</code></td>\n<td>بعد الـ render الأول، وكلما تغيّرت <code dir=\"ltr\">a</code> أو <code dir=\"ltr\">b</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn, [])</code></td>\n<td>بعد الـ render الأول فقط</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn)</code></td>\n<td>بعد كل render</td>\n</tr>\n</tbody>\n</table></div><p>وقاعدة ما يوضع في المصفوفة ليست خيارًا: <strong>اسرد كل قيمة من الـ component يقرؤها الـ effect</strong>، مثل الـ props والـ state والمتغيّرات المشتقّة منهما. وقاعدة الـ lint الخاصة بالاعتماديات في مشروع Vite (<code dir=\"ltr\">react/exhaustive-deps</code> في Oxlint، و<code dir=\"ltr\">react-hooks/exhaustive-deps</code> في ESLint) تتحقّق من ذلك نيابةً عنك. إذا أغفلت شيئًا، فسيظل الـ effect يستخدم قيمة قديمة، وهذا هو الخطأ في سؤال الاختبار الثالث.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">التنظيف</h2>\n<p>بعض الـ effects تبدأ شيئًا يجب إيقافه: مؤقّتًا، أو مستمع أحداث، أو اتصالًا. أرجِع دالة تلغيه:</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:#8250DF;--shiki-dark:#D2A8FF\">useEffect</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:#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\"> '/'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focusSearch</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\">  window.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, handleKeyDown);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> window.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, handleKeyDown);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}, []);</span></span></code></pre></div>\n<p>تستدعي React دالة التنظيف قبل تشغيل الـ effect مجددًا، وحين يُزال الـ component من الشاشة. ومن دونها، سيضيف كل تركيب مستمعًا آخر، وسيعمل الضغط على «/» عدّة مرات.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-rf-3-5-t1\"><defs><marker id=\"arrow-l-rf-3-5-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rf-3-5-t1\">بعد الـ render الأول والـ commit، يعمل الـ effect. وحين تتغيّر إحدى الاعتماديات، تعيد React الـ render، وتشغّل دالة التنظيف السابقة، ثم تشغّل الـ effect مجددًا. وحين يُلغى تركيب الـ component، تشغّل React دالة التنظيف مرة أخيرة.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"80\" y=\"103\" text-anchor=\"middle\">Render + commit</text>\n  <rect class=\"d-box-primary\" x=\"190\" y=\"70\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"250\" y=\"103\" text-anchor=\"middle\">Effect</text>\n  <rect class=\"d-box\" x=\"350\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"425\" y=\"96\" text-anchor=\"middle\">تغيّرت الاعتماديات:</text>\n  <text class=\"d-label\" x=\"425\" y=\"116\" text-anchor=\"middle\">re-render</text>\n  <rect class=\"d-box-warn\" x=\"540\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"615\" y=\"96\" text-anchor=\"middle\">التنظيف، ثم</text>\n  <text class=\"d-label\" x=\"615\" y=\"116\" text-anchor=\"middle\">الـ Effect مجددًا</text>\n  <path class=\"d-arrow\" d=\"M150 98 L188 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M310 98 L348 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M500 98 L538 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow d-dashed\" d=\"M615 126 C 615 190, 425 190, 425 128\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <rect class=\"d-box-warn\" x=\"190\" y=\"170\" width=\"160\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"270\" y=\"200\" text-anchor=\"middle\">Unmount: تنظيف</text>\n  <path class=\"d-arrow\" d=\"M250 126 L260 168\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <text class=\"d-label-muted\" x=\"560\" y=\"210\" text-anchor=\"middle\">يتكرّر مع كل تغيير</text>\n</svg><figcaption>دورة حياة الـ effect</figcaption></figure>\n<h3 id=\"sec-3\" tabindex=\"-1\">لماذا يعمل مرتين في وضع التطوير</h3>\n<p>مع <code dir=\"ltr\">&lt;StrictMode&gt;</code> (الموجود في <code dir=\"ltr\">main.jsx</code> لديك)، تفعل React شيئًا غريبًا أثناء التطوير: تركّب الـ component، وتشغّل الـ effects، ثم تشغّل دوال تنظيفها فورًا كأنها تلغي التركيب، ثم تشغّل الـ effects مجددًا. إنه اختبار ضغط. إذا كانت دالة التنظيف تلغي الـ effect بشكل صحيح، فسيتصرّف التطبيق بالطريقة نفسها، ولن ترى إلا سطرًا إضافيًا في السجل. وإن لم تكن كذلك، كأن تنسى إزالة مستمع، فستلاحظ الآن بدل أن تلاحظ في الإنتاج. لا تحاول منع التشغيل المزدوج؛ بل أصلح دالة التنظيف.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">حفظ Watchlist في localStorage</h2>\n<p>حفظ الـ state هو الـ effect النموذجي: كلما تغيّرت <code dir=\"ltr\">movies</code>، اكتبها في التخزين.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"2\"><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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> STORAGE_KEY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'watchlist.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\"> loadMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</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\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</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:#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\">(loadMovies);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</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\">    localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movies));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [movies]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>هناك تفصيلتان مهمّتان. التحميل يحدث في <strong>دالة التهيئة (initializer)</strong>، <code dir=\"ltr\">useState(loadMovies)</code>، لا في effect. تمرير الدالة (لا استدعاؤها) يعني أن React تقرأ التخزين للـ render الأول فقط، لا في كل render، فتظهر القائمة المحفوظة فورًا. أمّا التحميل في effect فسيرسم قائمة فارغة أولًا، وقد يكتب effect الحفظ تلك المصفوفة الفارغة فوق أفلامك المخزّنة. كما تستخدم <code dir=\"ltr\">loadMovies</code> كتلة <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code>، لأن التخزين قد يكون محظورًا أو يحتوي على بيانات تالفة، والقيمة المعطوبة يجب ألّا تُسقط التطبيق كله.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">قد لا تحتاج إلى effect</h2>\n<p>الـ effects مخرج طوارئ، وأكثر أخطائها شيوعًا تأتي من استخدامها حيث لا حاجة إليها. قبل أن تكتب واحدًا، افحص هذه الحالات الثلاث.</p>\n<p><strong>حساب شيء من الـ props أو الـ state.</strong> القوائم المصفّاة والأعداد والنصوص المنسّقة تُحسب أثناء الـ render. تخزينها في الـ state ومزامنتها بـ effect يضيف render إضافيًا ببيانات قديمة، ومزيدًا من الـ state الذي قد ينحرف.</p>\n<p><strong>الاستجابة لإجراء من المستخدم.</strong> إذا كان يجب أن يُنفَّذ الكود لأن المستخدم نقر Add، فضعه في معالج الإرسال. الـ effect الذي يراقب <code dir=\"ltr\">movies</code> لا يستطيع التمييز بين الإضافة والحذف وإعادة التحميل.</p>\n<p><strong>إعادة ضبط الـ state حين يتغيّر prop.</strong> بدل effect يمسح مسوّدة حين تتغيّر <code dir=\"ltr\">movieId</code>، امنح الـ component قيمة key: <code dir=\"ltr\">&lt;NotesEditor key={movieId} /&gt;</code>. الـ key الجديد يعني component جديدًا بـ state جديد.</p>\n<p>وما يتبقّى هو التزامن الحقيقي: واجهات المتصفّح، والتخزين، والمؤقّتات، والاشتراكات، واتصالات الشبكة. قائمة قصيرة، وهي القائمة الصحيحة.</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>الحلقة اللانهائية</span></p>\n<p>الـ effect الذي يضبط state موجودًا أيضًا في مصفوفة اعتمادياته، <code dir=\"ltr\">useEffect(() =&gt; setCount(count + 1), [count])</code>، يعيد التشغيل بعد كل تحديث إلى الأبد. ويحدث الشيء نفسه حين يسرد effect يضبط state كائنًا أو مصفوفة تُنشأ أثناء الـ render كاعتمادية، لأنها قيمة جديدة في كل render. إذا تجمّدت الصفحة أو أبلغت React بالخطأ «Maximum update depth exceeded»، فابحث عن effect يغذّي اعتمادياته بنفسه.</p>\n</aside>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">ملاحظة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>جلب البيانات</span></p>\n<p>يمكنك جلب البيانات داخل effect، لكن فعل ذلك بشكل صحيح يعني أن تتعامل بنفسك مع حالات السباق (race conditions) والتخزين المؤقّت وحالات التحميل. المشاريع الحقيقية تستخدم عادةً إطار عمل أو مكتبة مثل TanStack Query بدلًا من ذلك. سترى الخيارات في الدرس الأخير.</p>\n</aside>\n<p>بهذا يكتمل جوهر React: الـ components والـ props والـ state والأحداث والـ effects. في القسم التالي تجمع كل القطع في Watchlist المكتمل، وتنسّقه، وتنشره.</p>\n","toc":[{"id":"sec-1","text":"أول effect لك","level":2},{"id":"sec-2","text":"التنظيف","level":2},{"id":"sec-3","text":"لماذا يعمل مرتين في وضع التطوير","level":3},{"id":"sec-4","text":"حفظ Watchlist في localStorage","level":2},{"id":"sec-5","text":"قد لا تحتاج إلى effect","level":2}],"takeaways":["يعمل الـ effect بعد أن تحدّث React الشاشة، ووظيفته التزامن مع شيء خارج React، مثل عنوان المستند أو localStorage أو المؤقّتات أو الاشتراكات.","تسرد مصفوفة الاعتماديات كل قيمة من الـ component يقرؤها الـ effect؛ وتعيد React تشغيل الـ effect حين تتغيّر إحداها.","أرجِع دالة تنظيف (cleanup) تلغي ما فعله الـ effect؛ تشغّلها React قبل التشغيل التالي وعند إلغاء التركيب.","في وضع التطوير، يشغّل StrictMode كل effect، ثم دالة تنظيفه، ثم الـ effect من جديد، ليثبت أن التنظيف يعمل.","إذا كان يمكن حساب القيمة من الـ props أو الـ state، أو كان التغيير ناتجًا عن إجراء محدّد من المستخدم، فلا تحتاج إلى effect."],"further":[{"title":"Synchronizing with Effects","url":"https://react.dev/learn/synchronizing-with-effects"},{"title":"You Might Not Need an Effect","url":"https://react.dev/learn/you-might-not-need-an-effect"},{"title":"useEffect reference","url":"https://react.dev/reference/react/useEffect"}],"quiz":[{"q":"<p>تُحفظ <code dir=\"ltr\">visibleMovies</code> في الـ state وتُحدَّث بـ <code dir=\"ltr\">useEffect(() =&gt; setVisibleMovies(movies.filter(…)), [movies, filter])</code>. ما النهج الأفضل؟</p>","options":[{"text":"احسبها أثناء الـ render: <code dir=\"ltr\">const visibleMovies = movies.filter(…)</code>.","why":"صحيح. إنها بيانات مشتقّة. نسخة الـ effect ترسم مرة ببيانات قديمة، ثم مرة أخرى بالبيانات الصحيحة، وتضيف state قد ينحرف."},{"text":"أضف <code dir=\"ltr\">visibleMovies</code> إلى مصفوفة الاعتماديات أيضًا.","why":"هذا يجعل الـ effect يعيد التشغيل كلما ضبط مخرجاته بنفسه، فيفتح الباب لحلقة لا نهائية. الحلّ الحقيقي حذف الـ effect."},{"text":"انقل الفلترة إلى <code dir=\"ltr\">setTimeout</code> داخل الـ effect.","why":"تأخير العمل يطيل عمر الـ render القديم. لا شيء هنا يحتاج إلى effect."}],"answer":0},{"q":"<p>في وضع التطوير، effect يطبع «subscribed» وله دالة تنظيف تطبع «unsubscribed»، يطبع عند تحميل الصفحة «subscribed, unsubscribed, subscribed». ما الذي يحدث؟</p>","options":[{"text":"مصفوفة الاعتماديات غائبة، فيعمل الـ effect بعد كل render.","why":"من دون مصفوفة يُعاد تشغيل الـ effect بعد كل re-render، لكن تحميل الصفحة العادي فيه render واحد، فلا يفسّر ذلك وحده تنظيفًا مباشرًا بعد التشغيل الأول."},{"text":"يُرسم الـ component مرتين بسبب خطأ في الأب.","why":"هذا التسلسل بعينه مقصود، ولا يحدث إلا في وضع التطوير."},{"text":"يشغّل React 19 الـ effects مرتين دائمًا، في الإنتاج أيضًا.","why":"الدورة الإضافية فحص خاص بالتطوير من StrictMode. في الإنتاج يعمل الـ effect مرة واحدة."},{"text":"يركّب StrictMode الـ component ثم يلغي تركيبه ثم يركّبه من جديد أثناء التطوير، ليتحقّق أن التنظيف يلغي الـ effect بالكامل.","why":"صحيح. إذا تصرّف تطبيقك بالطريقة نفسها بعد هذه الدورة، فدالة التنظيف صحيحة. وإن انكسر، فالخطأ كان سيظهر في الإنتاج عاجلًا أم آجلًا."}],"answer":3},{"q":"<p>ما الخطأ في هذا الـ effect؟</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:#8250DF;--shiki-dark:#D2A8FF\">useEffect</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\">  document.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">left</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} to watch`</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","options":[{"text":"لا يمكن ضبط <code dir=\"ltr\">document.title</code> من React.","why":"ضبط عنوان المستند استخدام كلاسيكي وصحيح للـ effect؛ إنه API من المتصفّح خارج React."},{"text":"يقرأ <code dir=\"ltr\">left</code> لكنه لا يسردها، فيُضبط العنوان مرة واحدة ولا يتحدّث أبدًا حين تتغيّر <code dir=\"ltr\">left</code>.","why":"صحيح. المصفوفة الفارغة تعني «شغّل بعد الـ render الأول فقط». اسرد كل قيمة يقرؤها الـ effect، <code dir=\"ltr\">[left]</code>، وستذكّرك قاعدة الـ lint بذلك."},{"text":"يحتاج إلى دالة تنظيف وإلا رمت React خطأ.","why":"التنظيف اختياري. كثير من الـ effects، مثل هذا، لا يوجد ما تلغيه."},{"text":"الـ template literals لا تعمل داخل الـ effects.","why":"الـ effect دالة عادية؛ أي JavaScript تعمل بداخلها."}],"answer":1},{"q":"<p>بعد إضافة فيلم تريد عرض رسالة «Movie added». أين يجب أن يوضع الكود الذي يعرضها؟</p>","options":[{"text":"في effect يراقب <code dir=\"ltr\">movies</code> ويعرض الرسالة كلما تغيّرت.","why":"تتغيّر القائمة أيضًا حين تحذف أفلامًا أو تبدّل حالتها أو تحمّلها، فستظهر الرسالة في أوقات خاطئة."},{"text":"في effect بمصفوفة اعتماديات فارغة.","why":"هذا يعمل مرة واحدة بعد الـ render الأول، قبل أن يضيف أحد أي فيلم بوقت طويل."},{"text":"في معالج الإرسال، في المكان نفسه الذي يُضاف فيه الفيلم.","why":"صحيح. الرسالة ناتجة عن إجراء محدّد من المستخدم، فمكانها معالج ذلك الإجراء، لا effect."}],"answer":2}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useEffect, useState } from 'react';\n\n// The page's own title, before the Watchlist changes it.\ndocument.title = 'Playground';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival', watched: true },\n  { id: 'p2', title: 'Past Lives', watched: false },\n  { id: 'd3', title: 'Dune: Part Two', watched: false },\n];\n\nfunction Watchlist() {\n  const [movies, setMovies] = useState(INITIAL);\n  const [left, setLeft] = useState(0);\n\n  // 1. This Effect only copies derived data into state. Replace it.\n  useEffect(() => {\n    setLeft(movies.filter((m) => !m.watched).length);\n  }, [movies]);\n\n  // 2. Sync document.title, and restore the old title on cleanup.\n\n  function toggle(id) {\n    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));\n  }\n\n  return (\n    <section>\n      <p id=\"left\">{left} to watch</p>\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>\n            {m.title}\n            <button onClick={() => toggle(m.id)}>{m.watched ? 'Watched' : 'Mark watched'}</button>\n          </li>\n        ))}\n      </ul>\n    </section>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(true);\n  return (\n    <main>\n      <button id=\"hide\" onClick={() => setShow(false)}>Hide watchlist</button>\n      {show && <Watchlist />}\n    </main>\n  );\n}\n"},"solution":{"jsx":"import { useEffect, useState } from 'react';\n\n// The page's own title, before the Watchlist changes it.\ndocument.title = 'Playground';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival', watched: true },\n  { id: 'p2', title: 'Past Lives', watched: false },\n  { id: 'd3', title: 'Dune: Part Two', watched: false },\n];\n\nfunction Watchlist() {\n  const [movies, setMovies] = useState(INITIAL);\n  const left = movies.filter((m) => !m.watched).length;\n\n  useEffect(() => {\n    const previous = document.title;\n    document.title = `${left} to watch · Watchlist`;\n    return () => {\n      document.title = previous;\n    };\n  }, [left]);\n\n  function toggle(id) {\n    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));\n  }\n\n  return (\n    <section>\n      <p id=\"left\">{left} to watch</p>\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>\n            {m.title}\n            <button onClick={() => toggle(m.id)}>{m.watched ? 'Watched' : 'Mark watched'}</button>\n          </li>\n        ))}\n      </ul>\n    </section>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(true);\n  return (\n    <main>\n      <button id=\"hide\" onClick={() => setShow(false)}>Hide watchlist</button>\n      {show && <Watchlist />}\n    </main>\n  );\n}\n"},"checks":[{"id":"count-derived","test":"return text('#left') === '2 to watch';"},{"id":"title-initial","test":"return document.title === '2 to watch · Watchlist';"},{"id":"title-updates","test":"const toggle = (t) => $$('li').find((li) => text(li).startsWith(t)).querySelector('button'); await click(toggle('Past Lives')); return document.title === '1 to watch · Watchlist' && text('#left') === '1 to watch';"},{"id":"title-cleanup","test":"await click('#hide'); return !$('#left') && document.title === 'Playground';"}],"title":"زامن عنوان التبويب","prompt":"<p>مهمّتان في <code dir=\"ltr\">Watchlist</code>:</p>\n<ol>\n<li><strong>احذف الـ effect غير الضروري.</strong> تُنسخ <code dir=\"ltr\">left</code> إلى الـ state بواسطة effect. احسبها أثناء الـ render بدلًا من ذلك.</li>\n<li><strong>زامن عنوان تبويب المتصفّح.</strong> ما دام Watchlist على الشاشة، يجب أن تكون قيمة <code dir=\"ltr\">document.title</code> هي <strong>2 to watch · Watchlist</strong> (بالرقم الحقيقي)، وأن تتحدّث حين يُعلَّم فيلم كمُشاهَد. وحين يُزال Watchlist (انقر <strong>Hide watchlist</strong>)، يجب أن يعود العنوان إلى ما كان عليه قبل ذلك، أي <strong>Playground</strong>.</li>\n</ol>\n","hints":["استبدل state المسمّى <code dir=\"ltr\">left</code> والـ effect الخاص به بـ <code dir=\"ltr\">const left = movies.filter((m) =&gt; !m.watched).length;</code>","يقرأ effect العنوان القيمة <code dir=\"ltr\">left</code>، فتكون مصفوفة اعتمادياته <code dir=\"ltr\">[left]</code>.","احفظ العنوان القديم داخل الـ effect (<code dir=\"ltr\">const previous = document.title;</code>) وأرجِع دالة تنظيف تعيده."],"explanation":"<p>القيمة <code dir=\"ltr\">left</code> مشتقّة من <code dir=\"ltr\">movies</code>، فتُحسب أثناء الـ render. أمّا نسخة الـ effect فكانت ترسم مرة بعدد خاطئ هو 0 ثم مرة أخرى بالعدد الصحيح. في المقابل، يعيش عنوان المستند خارج React، فمزامنته effect حقيقي. سرد <code dir=\"ltr\">[left]</code> يعيد تشغيله فقط حين يتغيّر العدد، ودالة التنظيف تعيد العنوان السابق قبل كل إعادة تشغيل وحين يُلغى تركيب الـ component. وهذا بالضبط ما يختبره فحص StrictMode (تركيب، تنظيف، تركيب) أثناء التطوير.</p>\n","checkLabels":{"count-derived":"تعرض الصفحة “2 to watch”","title-initial":"عنوان التبويب هو “2 to watch · Watchlist”","title-updates":"تعليم Past Lives كمُشاهَد يحدّث العنوان إلى “1 to watch · Watchlist”","title-cleanup":"إخفاء Watchlist يعيد العنوان “Playground”"}},"words":726,"readMinutes":4}