{"id":"l-rf-4-1","courseId":"react-fundamentals","lang":"ar","kind":"lesson","summary":"اجمع Watchlist المكتمل من components صغيرة، مع الـ state عند مالك واحد، وقيم مشتقّة، ومعالجات تُمرَّر إلى الأسفل، وقائمة محفوظة في localStorage.","html":"<p>كل قطعة من Watchlist موجودة الآن، موزّعة على الدروس: الـ components من القسم 1، والـ props والقوائم من القسم 2، والـ state والنماذج والـ effects من القسم 3. هذا الدرس يجمعها في تطبيق واحد ستكون مرتاحًا لعرضه في مقابلة عمل، ويشرح كل قرار لتتّخذ القرارات نفسها في مشروعك التالي.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">شكل التطبيق</h2>\n<p>هذا هو الترتيب النهائي للملفات في مشروع Vite:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</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>src/\n  main.jsx\n  App.jsx               state, handlers, derived values\n  storage.js            load and save to localStorage\n  components/\n    Header.jsx          title and &quot;N to watch&quot;\n    AddMovieForm.jsx    controlled title input\n    FilterBar.jsx       All / To watch / Watched\n    MovieList.jsx       list or empty state\n    MovieItem.jsx       one movie: toggle and remove</code></pre></div>\n<p>ملفّان فقط فيهما منطق يستحق الاختبار: <code dir=\"ltr\">App.jsx</code> الذي يملك الـ state، و<code dir=\"ltr\">storage.js</code>. وكل component في <code dir=\"ltr\">components/</code> يستقبل props ويُرجع JSX. هذا هو التقسيم الذي تريده: طبقة رقيقة من الـ state والمنطق في الأعلى، وطبقة عريضة من القطع البسيطة تحتها.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">التخزين، معزولًا</h2>\n<p>ابدأ بالـ module الذي يتحدّث مع المتصفّح:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/storage.js</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\"> 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\">export</span><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\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> saved</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</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\">KEY</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\"> Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isArray</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(saved) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> saved </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\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> saveMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">movies</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:#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\">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\">  } </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:#6E7781;--shiki-dark:#8B949E\">    // Storage full or blocked: the app still works, it just won't remember.</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></code></pre></div>\n<p>قد يرمي localStorage أخطاء (محظور في بعض أوضاع الخصوصية، أو امتلأت سعته)، وقد يحتوي على أي شيء، بما في ذلك بيانات من نسخة أقدم من تطبيقك. فحص <code dir=\"ltr\">Array.isArray</code> والتقاط الأخطاء يعنيان أن القيمة المعطوبة تعطيك قائمة فارغة بدل شاشة بيضاء. ولا ترى الـ components أي شيء من هذا.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الـ App: المالك</h2>\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\">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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { loadMovies, saveMovies } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './storage.js'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Header </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './components/Header.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> AddMovieForm </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './components/AddMovieForm.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> FilterBar </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './components/FilterBar.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> MovieList </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './components/MovieList.jsx'</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\">(loadMovies);</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\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setFilter</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:#0A3069;--shiki-dark:#A5D6FF\">'all'</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:#8250DF;--shiki-dark:#D2A8FF\">    saveMovies</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:#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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> visible</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    filter </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'all'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> filter </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m.watched </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>\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\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">title</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\">([</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movies, { id: crypto.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">randomUUID</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), title, watched: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleToggle</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\">map</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 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:#1F2328;--shiki-dark:#E6EDF3\">m, watched: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m.watched } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m)));</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\">  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\"> (</span></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\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"app\"</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\">Header</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\">left</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> total</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movies.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</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\">AddMovieForm</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onAdd</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleAdd</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\">FilterBar</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">filter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onChange</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">setFilter</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\">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\">visible</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onToggle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleToggle</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\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</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></code></pre></div>\n<p>اقرأه من الأعلى إلى الأسفل وستعرف التطبيق كله. قطعتان من الـ state. effect واحد، للشيء الوحيد الموجود خارج React. قيمتان مشتقّتان. ثلاثة معالجات، كلٌّ منها سطر واحد، وكلٌّ منها إجراء مسمّى يستطيع المستخدم القيام به. ثم التخطيط. وحين يصل بلاغ خطأ («حذف فيلم يعيده بعد تحديث الصفحة»)، تعرف بالضبط أي الأسطر قد تكون معنية.</p>\n<p>تأتي المعرّفات من <code dir=\"ltr\">crypto.randomUUID()</code>، وتُنشأ مرة واحدة في <code dir=\"ltr\">handleAdd</code> وتُخزَّن مع الفيلم، تمامًا كما أوصى درس الـ keys. وهي تعمل على <code dir=\"ltr\">localhost</code> وعبر HTTPS، وهذا يغطّي التطوير وVercel.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">القطع التي تحت</h2>\n<p>الـ components الخاصة بالقائمة والعنصر قصيرة لأنها لا تفعل سوى عرض الـ props وتمرير الأحداث:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/components/MovieList.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\"> MovieItem </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './MovieItem.jsx'</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\"> MovieList</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({ movies, onToggle, onRemove }) </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\"> (movies.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"empty\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>No movies here yet.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</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\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"movie-list\"</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\">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\">MovieItem</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:#0550AE;--shiki-dark:#79C0FF\"> movie</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onToggle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">onToggle</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\">onRemove</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\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</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></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">src/components/MovieItem.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\"> MovieItem</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({ movie, onToggle, onRemove }) </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\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'movie watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'movie'</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\">span</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"title\"</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\">span</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.watched</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\"> onToggle</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.watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Mark watched'</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:#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 style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Remove ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">title</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">></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\">li</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></code></pre></div>\n<p>أمّا <code dir=\"ltr\">AddMovieForm</code> و<code dir=\"ltr\">FilterBar</code> فهما النسختان اللتان بنيتهما في الدروس السابقة، وصار <code dir=\"ltr\">Header</code> يستقبل الرقمين اللذين يعرضهما بدل القائمة كاملة. ويحصل زر Remove على <code dir=\"ltr\">aria-label</code> يسمّي الفيلم، لأن مستخدم الـ screen reader الذي يتنقّل بمفتاح Tab بين عشرة أزرار «Remove» يحتاج إلى معرفة الزر الذي يقف عليه.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 230\" role=\"img\" aria-labelledby=\"l-rf-4-1-t1\"><defs><marker id=\"arrow-l-rf-4-1-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-4-1-t1\">النقر على Mark watched في MovieItem يستدعي onToggle، فتُنفَّذ handleToggle في App. تُطلق setMovies إعادة render؛ فتنزل props جديدة إلى Header وMovieList، ويكتب effect الحفظ في localStorage.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"30\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"85\" y=\"54\" text-anchor=\"middle\">MovieItem</text>\n  <text class=\"d-code\" x=\"85\" y=\"74\" text-anchor=\"middle\">onToggle(id)</text>\n  <rect class=\"d-box-primary\" x=\"200\" y=\"30\" width=\"170\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"285\" y=\"54\" text-anchor=\"middle\">App</text>\n  <text class=\"d-code\" x=\"285\" y=\"74\" text-anchor=\"middle\">setMovies(…)</text>\n  <rect class=\"d-box\" x=\"410\" y=\"30\" width=\"130\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"475\" y=\"63\" text-anchor=\"middle\">Re-render</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"10\" width=\"110\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"635\" y=\"37\" text-anchor=\"middle\">Header</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"64\" width=\"110\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"635\" y=\"91\" text-anchor=\"middle\">MovieList</text>\n  <rect class=\"d-box-accent\" x=\"410\" y=\"150\" width=\"200\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"510\" y=\"174\" text-anchor=\"middle\">effect بعد الـ commit</text>\n  <text class=\"d-code\" x=\"510\" y=\"194\" text-anchor=\"middle\">saveMovies(movies)</text>\n  <path class=\"d-arrow\" d=\"M160 58 L198 58\" marker-end=\"url(#arrow-l-rf-4-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M370 58 L408 58\" marker-end=\"url(#arrow-l-rf-4-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M540 50 L578 34\" marker-end=\"url(#arrow-l-rf-4-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M540 66 L578 84\" marker-end=\"url(#arrow-l-rf-4-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M475 86 L490 148\" marker-end=\"url(#arrow-l-rf-4-1-1)\"/>\n</svg><figcaption>نقرة واحدة، من البداية إلى النهاية</figcaption></figure>\n<h2 id=\"sec-5\" tabindex=\"-1\">لماذا تصمد هذه البنية</h2>\n<p>انظر إلى ما يحتاجه كل component ليُفحص. يحتاج <code dir=\"ltr\">MovieList</code> إلى مصفوفة ودالتين. أعطه مصفوفة فارغة ويجب أن يعرض «No movies here yet.»؛ وأعطه فيلمين ويجب أن يرسم عنصرين. ويحتاج <code dir=\"ltr\">MovieItem</code> إلى فيلم واحد، والنقر على Remove يجب أن يستدعي <code dir=\"ltr\">onRemove</code> بمعرّف ذلك الفيلم. لا يحتاج أيٌّ منهما إلى localStorage، ولا إلى نموذج حقيقي، ولا إلى بقية التطبيق. هذا ما تعنيه «قطع صغيرة قابلة للاختبار» عمليًا: لكل قطعة بضعة مدخلات وبضعة مخرجات مرئية.</p>\n<p>وحين تضيف اختبارات آلية لاحقًا (Vitest مع React Testing Library هو الثنائي الشائع لمشاريع Vite)، فهذه هي الاختبارات التي ستكتبها أولًا. وحتى قبل أن تكون لديك اختبارات، تؤتي البنية ثمارها: حين تبدو الحالة الفارغة خاطئة، تفتح ملفًا واحدًا من 10 أسطر، لا component من 300 سطر.</p>\n<h2 id=\"sec-6\" tabindex=\"-1\">حين تكبر المعالجات</h2>\n<p>ثلاثة معالجات من سطر واحد سهلة المتابعة. لكن إن أضفت التعديل وإعادة الترتيب والتراجع، فسيمتلئ <code dir=\"ltr\">App</code> بمنطق التحديث. هذه هي اللحظة التي تفكّر فيها في <code dir=\"ltr\">useReducer</code>: إنه ينقل كل قواعد «كيف تتغيّر الأفلام» إلى دالة نقية واحدة، <code dir=\"ltr\">moviesReducer(movies, action)</code>، يمكنك اختبارها من دون رسم أي شيء. لا تبدأ من هناك. ابدأ بـ <code dir=\"ltr\">useState</code>، والجأ إلى reducer حين تحدّث عدّة معالجات الـ state نفسه بطرق مترابطة ويصبح الـ component صعب القراءة.</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>تضيف فيلمًا، فيظهر، فتعتبر أنك انتهيت. ثم يحدّث مستخدمٌ الصفحة فيخسر كل شيء، أو يضغط Enter في حقل فارغ فيحصل على صف فارغ. قبل النشر، امشِ في كل مسار بيدك: أضف بالفأرة وبمفتاح Enter؛ جرّب عنوانًا فارغًا وعنوانًا من مسافات فقط؛ بدّل واحذف وصفِّ؛ حدّث الصفحة؛ أفرغ القائمة وتحقّق من الرسالة؛ تنقّل في التطبيق كله بلوحة المفاتيح وحدها.</p>\n</aside>\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>اعمل commit عند هذه النقطة</span></p>\n<p>نفّذ <code dir=\"ltr\">git init</code>، ثم <code dir=\"ltr\">git add .</code> و<code dir=\"ltr\">git commit -m &quot;Watchlist: add, toggle, remove, filter, persist&quot;</code>. سترفع هذا المستودع إلى GitHub قبل النشر، ومن الممارسات الجيدة أن تعمل commit لحالة تعمل قبل أن تبدأ بإعادة تنسيقها.</p>\n</aside>\n<p>Watchlist يعمل. لكن شكله ليس مميّزًا بعد، وهذا موضوع الدرس التالي: تنسيقه باستخدام Tailwind CSS.</p>\n","toc":[{"id":"sec-1","text":"شكل التطبيق","level":2},{"id":"sec-2","text":"التخزين، معزولًا","level":2},{"id":"sec-3","text":"الـ App: المالك","level":2},{"id":"sec-4","text":"القطع التي تحت","level":2},{"id":"sec-5","text":"لماذا تصمد هذه البنية","level":2},{"id":"sec-6","text":"حين تكبر المعالجات","level":2}],"takeaways":["يحتفظ التطبيق المكتمل بقطعتين فقط من الـ state الحقيقي في App، هما الأفلام والفلتر، ويشتقّ كل ما عداهما أثناء الـ render.","كل التغييرات على الأفلام تحدث في عدد قليل من المعالجات المسمّاة داخل App، ويستدعيها الأبناء عبر الـ props.","الـ components العرضية الصغيرة التي لا تفعل سوى استقبال الـ props سهلة القراءة وإعادة الاستخدام والاختبار.","اعزل واجهات المتصفّح مثل localStorage في module صغير مع معالجة للأخطاء، لتبقى الـ components بسيطة.","امشِ في كل مسار استخدام بيدك، بما فيه تحديث الصفحة والحالات الفارغة والاستخدام بلوحة المفاتيح، قبل أن تعدّ العمل منتهيًا."],"further":[{"title":"Thinking in React","url":"https://react.dev/learn/thinking-in-react"},{"title":"Extracting State Logic into a Reducer","url":"https://react.dev/learn/extracting-state-logic-into-a-reducer"},{"title":"Window.localStorage (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"}],"quiz":[{"q":"<p>توسّع Watchlist المكتمل بزر «Clear watched» يكون معطّلًا حين لا يوجد فيلم مُشاهَد، إلى جانب العدد والقائمة المصفّاة. كم استدعاءً لـ <code dir=\"ltr\">useState</code> يحتاج App؟</p>","options":[{"text":"خمسة، واحد لكلٍّ من الأفلام والفلتر والعدد والأفلام الظاهرة وحالة تعطيل الزر.","why":"العدد والقائمة الظاهرة وحالة التعطيل كلها تُحسب من الأفلام والفلتر. وتخزينها ينشئ نُسخًا تنحرف."},{"text":"واحد، كائن يحمل كل شيء.","why":"يمكنك ذلك، لكن الأفلام والفلتر يتغيّران باستقلال. قطعتان منفصلتان من الـ state أبسط في التحديث."},{"text":"اثنان، للأفلام والفلتر. وكل ما عداهما يُشتقّ أثناء الـ render.","why":"صحيح. مسوّدة العنوان تعيش في النموذج، وكل ما عدا ذلك يُحسب من هاتين القطعتين."}],"answer":2},{"q":"<p>يعلّم المستخدم فيلمًا كمُشاهَد بالنقر على زرّه في <code dir=\"ltr\">MovieItem</code>. بأيّ ترتيب تحدث الأشياء؟</p>","options":[{"text":"يضبط MovieItem الـ state الخاص به <code dir=\"ltr\">watched</code>، ثم يُخبر App عبر effect.","why":"لا يملك MovieItem أي state خاص به في هذا التصميم، ومزامنة الـ state عبر effect هي النمط الذي يجب تجنّبه."},{"text":"يستدعي MovieItem الدالة <code dir=\"ltr\">onToggle(id)</code>، فيستدعي معالج App الدالة <code dir=\"ltr\">setMovies</code>، ويعيد App الـ render، وتنزل الـ props الجديدة إلى الترويسة والقائمة.","why":"صحيح. تصعد الأحداث إلى المالك، ويحدّث المالك الـ state، وتنزل البيانات الجديدة إلى كل component يعرضها."},{"text":"يفحص App الـ DOM دوريًا بحثًا عن مربّعات الاختيار المعلَّمة ويحدّث الـ state الخاص به.","why":"لا تقرأ React الـ state من الـ DOM أبدًا. الـ DOM يُرسم من الـ state، لا العكس."},{"text":"يعدّل MovieItem كائن الفيلم الذي استقبله، فتلاحظ React التغيير.","why":"الـ props للقراءة فقط، وReact لا تكتشف التعديلات المباشرة. لن يُعاد رسم أي شيء."}],"answer":1},{"q":"<p>لماذا يضع الدرس <code dir=\"ltr\">loadMovies</code> و<code dir=\"ltr\">saveMovies</code> في module منفصل اسمه <code dir=\"ltr\">storage.js</code>؟</p>","options":[{"text":"ليبقى API المتصفّح ومعالجة أخطائه ومفتاح التخزين في مكان واحد، فلا تتعامل الـ components إلا مع مصفوفات عادية.","why":"صحيح. إذا انتقلت لاحقًا إلى خادم أو IndexedDB، فستغيّر ملفًا صغيرًا واحدًا، ويبقى كود App كما هو."},{"text":"لأن الـ components في React لا يُسمح لها باستدعاء localStorage.","why":"يمكن للـ components استدعاء أي API من المتصفّح داخل المعالجات والـ effects. الفصل مسألة وضوح، لا إذن."},{"text":"لأن localStorage لا يعمل إلا في الملفات التي تنتهي بـ <code dir=\"ltr\">.js</code>.","why":"امتداد الملف لا يغيّر شيئًا في واجهات المتصفّح. ملفات <code dir=\"ltr\">.jsx</code> تشغّل JavaScript نفسها."}],"answer":0},{"q":"<p>بعد إضافة «Clear watched» صار في App معالجات للإضافة والتبديل والحذف ومسح المُشاهَد، وكلها تستدعي <code dir=\"ltr\">setMovies</code> بمنطق مختلف. متى يستحق <code dir=\"ltr\">useReducer</code> العناء؟</p>","options":[{"text":"فورًا، لأن useState لا يجب أن يُستخدم مع المصفوفات.","why":"تتعامل useState مع المصفوفات بشكل ممتاز، كما يُثبت هذا التطبيق كله."},{"text":"فقط حين يستخدم التطبيق TypeScript.","why":"تعمل الـ reducers بالطريقة نفسها في JavaScript وTypeScript؛ اللغة ليست العامل الحاسم."},{"text":"أبدًا، لأن useReducer أصبح متوقّفًا (deprecated) في React 19.","why":"الـ useReducer هو hook حالي ومدعوم بالكامل. لم يُوقف React 19 أي شيء فيه."},{"text":"حين يكبر منطق التحديث بما يكفي لأن يصبح جمعه في دالة نقية واحدة، خارج الـ component، أسهل في القراءة والاختبار.","why":"صحيح. ينقل الـ reducer «كيف يتغيّر الـ state» إلى دالة واحدة يمكنك اختبارها من دون render. ومع أربعة معالجات قصيرة، ما زال useState مناسبًا."}],"answer":3}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useState } from 'react';\n\nconst INITIAL = [\n  { id: 1, title: 'Arrival', watched: true },\n  { id: 2, title: 'Past Lives', watched: false },\n  { id: 3, title: 'Dune: Part Two', watched: false },\n];\nlet nextId = 4; // in a real app: crypto.randomUUID()\n\nfunction AddMovieForm({ onAdd }) {\n  const [title, setTitle] = useState('');\n  const trimmed = title.trim();\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    if (!trimmed) return;\n    onAdd(trimmed);\n    setTitle('');\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <input id=\"title\" aria-label=\"Movie title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n      <button type=\"submit\" disabled={!trimmed}>Add</button>\n    </form>\n  );\n}\n\nfunction FilterBar({ value, onChange }) {\n  const options = [['all', 'All'], ['to-watch', 'To watch'], ['watched', 'Watched']];\n  return (\n    <div className=\"filters\">\n      {options.map(([key, label]) => (\n        <button key={key} aria-pressed={value === key} onClick={() => onChange(key)}>\n          {label}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nfunction MovieItem({ movie, onToggle, onRemove }) {\n  return (\n    <li>\n      <span className=\"title\">{movie.title}</span>\n      <button className=\"toggle\" aria-pressed={movie.watched} onClick={() => onToggle(movie.id)}>\n        {movie.watched ? 'Watched' : 'Mark watched'}\n      </button>\n      <button className=\"remove\" aria-label={`Remove ${movie.title}`} onClick={() => onRemove(movie.id)}>\n        Remove\n      </button>\n    </li>\n  );\n}\n\nfunction MovieList({ movies, onToggle, onRemove }) {\n  if (movies.length === 0) {\n    return <p className=\"empty\">No movies here yet.</p>;\n  }\n  return (\n    <ul>\n      {movies.map((movie) => (\n        <MovieItem key={movie.id} movie={movie} onToggle={onToggle} onRemove={onRemove} />\n      ))}\n    </ul>\n  );\n}\n\nexport default function App() {\n  // Wire it up: state for movies and filter, derived values, and handlers.\n  const movies = INITIAL;\n\n  return (\n    <main>\n      <h1>Watchlist</h1>\n      <p id=\"count\">0 to watch</p>\n      <AddMovieForm onAdd={() => {}} />\n      <FilterBar value=\"all\" onChange={() => {}} />\n      <MovieList movies={movies} onToggle={() => {}} onRemove={() => {}} />\n    </main>\n  );\n}\n"},"solution":{"jsx":"import { useState } from 'react';\n\nconst INITIAL = [\n  { id: 1, title: 'Arrival', watched: true },\n  { id: 2, title: 'Past Lives', watched: false },\n  { id: 3, title: 'Dune: Part Two', watched: false },\n];\nlet nextId = 4; // in a real app: crypto.randomUUID()\n\nfunction AddMovieForm({ onAdd }) {\n  const [title, setTitle] = useState('');\n  const trimmed = title.trim();\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    if (!trimmed) return;\n    onAdd(trimmed);\n    setTitle('');\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <input id=\"title\" aria-label=\"Movie title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n      <button type=\"submit\" disabled={!trimmed}>Add</button>\n    </form>\n  );\n}\n\nfunction FilterBar({ value, onChange }) {\n  const options = [['all', 'All'], ['to-watch', 'To watch'], ['watched', 'Watched']];\n  return (\n    <div className=\"filters\">\n      {options.map(([key, label]) => (\n        <button key={key} aria-pressed={value === key} onClick={() => onChange(key)}>\n          {label}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nfunction MovieItem({ movie, onToggle, onRemove }) {\n  return (\n    <li>\n      <span className=\"title\">{movie.title}</span>\n      <button className=\"toggle\" aria-pressed={movie.watched} onClick={() => onToggle(movie.id)}>\n        {movie.watched ? 'Watched' : 'Mark watched'}\n      </button>\n      <button className=\"remove\" aria-label={`Remove ${movie.title}`} onClick={() => onRemove(movie.id)}>\n        Remove\n      </button>\n    </li>\n  );\n}\n\nfunction MovieList({ movies, onToggle, onRemove }) {\n  if (movies.length === 0) {\n    return <p className=\"empty\">No movies here yet.</p>;\n  }\n  return (\n    <ul>\n      {movies.map((movie) => (\n        <MovieItem key={movie.id} movie={movie} onToggle={onToggle} onRemove={onRemove} />\n      ))}\n    </ul>\n  );\n}\n\nexport default function App() {\n  const [movies, setMovies] = useState(INITIAL);\n  const [filter, setFilter] = useState('all');\n\n  const left = movies.filter((m) => !m.watched).length;\n  const visible = movies.filter((m) =>\n    filter === 'all' ? true : filter === 'watched' ? m.watched : !m.watched,\n  );\n\n  function handleAdd(title) {\n    setMovies([...movies, { id: nextId++, title, watched: false }]);\n  }\n\n  function handleToggle(id) {\n    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));\n  }\n\n  function handleRemove(id) {\n    setMovies(movies.filter((m) => m.id !== id));\n  }\n\n  return (\n    <main>\n      <h1>Watchlist</h1>\n      <p id=\"count\">{left} to watch</p>\n      <AddMovieForm onAdd={handleAdd} />\n      <FilterBar value={filter} onChange={setFilter} />\n      <MovieList movies={visible} onToggle={handleToggle} onRemove={handleRemove} />\n    </main>\n  );\n}\n"},"checks":[{"id":"count","test":"return text('#count') === '2 to watch';"},{"id":"add","test":"const titles = () => $$('li .title').map((s) => text(s)).join('|'); const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); const filterBtn = (t) => $$('.filters button').find((b) => text(b) === t); await input('#title', 'Paddington 2'); const __f = $('form'); if (__f) __f.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); else await click($('button')); await tick(20); return titles() === 'Arrival|Past Lives|Dune: Part Two|Paddington 2' && text('#count') === '3 to watch';"},{"id":"toggle","test":"const titles = () => $$('li .title').map((s) => text(s)).join('|'); const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); const filterBtn = (t) => $$('.filters button').find((b) => text(b) === t); await click(row('Past Lives').querySelector('.toggle')); return text('#count') === '2 to watch' && text(row('Past Lives').querySelector('.toggle')) === 'Watched';"},{"id":"filter","test":"const titles = () => $$('li .title').map((s) => text(s)).join('|'); const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); const filterBtn = (t) => $$('.filters button').find((b) => text(b) === t); await click(filterBtn('Watched')); if (titles() !== 'Arrival|Past Lives') return false; await click(filterBtn('To watch')); return titles() === 'Dune: Part Two|Paddington 2';"},{"id":"remove","test":"const titles = () => $$('li .title').map((s) => text(s)).join('|'); const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); const filterBtn = (t) => $$('.filters button').find((b) => text(b) === t); await click(row('Dune: Part Two').querySelector('.remove')); return titles() === 'Paddington 2' && text('#count') === '1 to watch';"},{"id":"empty","test":"const titles = () => $$('li .title').map((s) => text(s)).join('|'); const row = (t) => $$('li').find((li) => text(li.querySelector('.title')) === t); const filterBtn = (t) => $$('.filters button').find((b) => text(b) === t); await click(row('Paddington 2').querySelector('.remove')); return !$('ul') && text('.empty') === 'No movies here yet.' && text('#count') === '0 to watch';"}],"title":"اجمع تطبيق Watchlist","prompt":"<p>كل الـ components مكتوبة، لكن <code dir=\"ltr\">App</code> غير موصول. اجعله يعمل:</p>\n<ul>\n<li>احتفظ بـ <strong>الأفلام</strong> و<strong>الفلتر</strong> (<code dir=\"ltr\">'all'</code> و<code dir=\"ltr\">'to-watch'</code> و<code dir=\"ltr\">'watched'</code>) في الـ state، بدءًا من <code dir=\"ltr\">INITIAL</code> و<code dir=\"ltr\">'all'</code>.</li>\n<li>يعرض <code dir=\"ltr\">#count</code> عدد الأفلام غير المُشاهَدة بالشكل <strong>N to watch</strong>.</li>\n<li>الإضافة عبر النموذج تُلحق فيلمًا جديدًا غير مُشاهَد.</li>\n<li>زر <strong>Mark watched</strong> يبدّل حالة الفيلم، وزر <strong>Remove</strong> يحذفه، وأزرار الفلترة تغيّر الأفلام التي يستقبلها <code dir=\"ltr\">MovieList</code>.</li>\n</ul>\n<p>غيّر <code dir=\"ltr\">App</code> فقط. (الحفظ في localStorage مستبعد هنا: ساحة التجربة تحظر التخزين. في مشروع Vite الخاص بك، استخدم الـ module المسمّى <code dir=\"ltr\">storage.js</code> من الدرس.)</p>\n","hints":["استدعاءان لـ <code dir=\"ltr\">useState</code>: <code dir=\"ltr\">useState(INITIAL)</code> و<code dir=\"ltr\">useState('all')</code>.","اشتقّ <code dir=\"ltr\">left</code> و<code dir=\"ltr\">visible</code> منهما أثناء الـ render؛ لا يجب أن يكون أيٌّ منهما state.","ثلاثة معالجات: الإضافة (<code dir=\"ltr\">[...movies, { id: nextId++, title, watched: false }]</code>)، والتبديل (<code dir=\"ltr\">movies.map(…)</code> مع spread)، والحذف (<code dir=\"ltr\">movies.filter(…)</code>). مرّرها مع <code dir=\"ltr\">setFilter</code> إلى الأسفل."],"explanation":"<p>يملك <code dir=\"ltr\">App</code> قطعتين بالضبط من الـ state. العدد والقائمة الظاهرة يُشتقّان في كل render، فلا يمكن أن يختلفا أبدًا عن الأفلام. وكل معالج يستبدل المصفوفة بدل تعديلها (spread للإضافة، و<code dir=\"ltr\">map</code> للتبديل، و<code dir=\"ltr\">filter</code> للحذف)، والأبناء لا يفعلون سوى استدعاء الدوال التي تُعطى لهم. لاحظ أن الحالة الفارغة وأزرار <code dir=\"ltr\">aria-pressed</code> والحقل المشذَّب جاءت كلها مجانًا من components كتبتها من قبل: لم يتبقَّ إلا التوصيل.</p>\n","checkLabels":{"count":"يعرض “2 to watch” في البداية","add":"إضافة “Paddington 2” تُلحقه بالقائمة وتحدّث العدد","toggle":"زر Mark watched على Past Lives يحدّث زرّه والعدد","filter":"الفلتران Watched وTo watch يعرضان الأفلام الصحيحة","remove":"زر Remove يحذف فيلمًا ويحدّث العدد","empty":"حذف آخر فيلم ظاهر يعرض “No movies here yet.”"}},"words":639,"readMinutes":4}