{"id":"l-rf-3-3","courseId":"react-fundamentals","lang":"ar","kind":"lesson","summary":"ابنِ نموذج إضافة الأفلام في Watchlist باستخدام الـ controlled inputs، وتعامل مع الإرسال والتحقّق من المدخلات، واعرف متى تكون الـ form actions وuseActionState في React 19 الخيار الأنسب.","html":"<p>كل تطبيق مفيد يستقبل مدخلات. في Watchlist يعني ذلك نموذجًا: اكتب عنوانًا، وسنة إن أردت، واضغط Enter أو انقر Add، فيظهر الفيلم في القائمة. النماذج هي المكان الذي يلتقي فيه الـ state بالأحداث، وفيها يعيش معظم أخطاء المبتدئين: حقول ترفض الكتابة، وصفحات يُعاد تحميلها عند الإرسال، وأرقام تُلصق ببعضها كالنصوص. ولكل واحد منها سبب واضح.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ controlled input</h2>\n<p>في HTML العادي يملك الحقل قيمته، وأنت تقرؤها حين تحتاجها. أمّا في React فالأسلوب المعتاد يقلب ذلك: <strong>الـ state يملك القيمة</strong>، والحقل يعرضها.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/components/AddMovieForm.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\"> { 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\"> AddMovieForm</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\">title</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setTitle</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\">''</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\">  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\">form</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\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> htmlFor</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\">>Title&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</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\">input</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"title\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        onChange</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:#8250DF;--shiki-dark:#D2A8FF\"> setTitle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.value)</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\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">title.</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\">/80 characters&#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\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">form</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>هذا هو الـ <strong>controlled input</strong> (الحقل المتحكَّم به). كل ضغطة مفتاح تُطلق <code dir=\"ltr\">onChange</code>، فيخزّن المعالج النص الجديد في الـ state، وتعيد React الـ render، ويعرض الحقل الـ state. يبدو هذا التفافًا، لكن النص الحالي صار الآن متغيّرًا عاديًا. يمكنك عرض عدد الحروف، والتحقّق أثناء الكتابة، وتعطيل زر، أو مسح الحقل بضبط الـ state على <code dir=\"ltr\">''</code>.</p>\n<p>لاحظ أن <code dir=\"ltr\">onChange</code> في React يعمل مع كل ضغطة مفتاح، مثل حدث <code dir=\"ltr\">input</code> في الـ DOM، لا فقط حين يفقد الحقل التركيز.</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>الـ <code dir=\"ltr\">value</code> من دون <code dir=\"ltr\">onChange</code></span></p>\n<p>الكود <code dir=\"ltr\">&lt;input value={title} /&gt;</code> وحده يصنع حقلًا للقراءة فقط: كل render يعيد <code dir=\"ltr\">title</code> إليه، فلا تفعل الكتابة شيئًا، وتحذّر React في الـ console. إمّا أن تضيف <code dir=\"ltr\">onChange</code>، وإمّا، إن لم تكن تحتاج القيمة في الـ state، أن تستخدم <code dir=\"ltr\">defaultValue</code> لتجعله غير متحكَّم به (uncontrolled). وهناك تحذير قريب، «changing an uncontrolled input to be controlled»، يعني أن <code dir=\"ltr\">value</code> بدأت بقيمة <code dir=\"ltr\">undefined</code>؛ فابدأ الـ state بـ <code dir=\"ltr\">''</code>.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">الإرسال</h2>\n<p>تعامل مع الإرسال على النموذج، لا على نقرة الزر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/components/AddMovieForm.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\"> AddMovieForm</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({ onAdd }) </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\">title</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setTitle</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\">''</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\"> trimmed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> title.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</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\"> handleSubmit</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:#1F2328;--shiki-dark:#E6EDF3\">    e.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">preventDefault</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\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">trimmed) </span><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:#8250DF;--shiki-dark:#D2A8FF\">    onAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(trimmed);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setTitle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</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\">form</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSubmit</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleSubmit</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\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> htmlFor</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\">>Title&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</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\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"title\"</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\">title</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\">(</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:#8250DF;--shiki-dark:#D2A8FF\"> setTitle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.value)</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\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">trimmed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add&#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\">form</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\">onSubmit</code> على النموذج يعني أن الضغط على Enter داخل الحقل يعمل، والزر يعمل، والتقنيات المساعدة تفهم النموذج، كل ذلك من دون كود إضافي. وتوقف <code dir=\"ltr\">e.preventDefault()</code> السلوك الافتراضي للمتصفّح، وهو إرسال النموذج إلى رابط وإعادة تحميل الصفحة. ويبلّغ الـ component عن العنوان الجديد عبر <code dir=\"ltr\">onAdd</code>، ويمسح الحقل بإعادة ضبط الـ state.</p>\n<p>والتحقّق يأتي من الـ state أيضًا. تُشتقّ <code dir=\"ltr\">trimmed</code> في كل render، فيكون الزر معطّلًا بالضبط ما دام العنوان فارغًا، بما في ذلك العنوان المكوّن من مسافات فقط. أمّا <code dir=\"ltr\">if (!trimmed) return;</code> داخل المعالج فحارس ثانٍ تحسّبًا لإرسال النموذج بطريقة أخرى.</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>حين تعرض رسالة مثل «Title is required»، اربطها بالحقل باستخدام <code dir=\"ltr\">aria-describedby</code> الذي يشير إلى <code dir=\"ltr\">id</code> الرسالة، واضبط <code dir=\"ltr\">aria-invalid={true}</code> على الحقل. عندها تعلن الـ screen readers الخطأ حين يحصل الحقل على التركيز.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">أنواع أخرى من الحقول</h2>\n<p>كل أنواع الحقول تتبع النمط نفسه مع فروق صغيرة:</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:#6E7781;--shiki-dark:#8B949E\">// Numbers arrive as strings. Keep the string in state, convert on use.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"number\"</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\">year</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\">(</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:#8250DF;--shiki-dark:#D2A8FF\"> setYear</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.value)</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> yearNumber</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> year </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ''</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(year);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Checkboxes use checked, not value.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"checkbox\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> checked</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched</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\">(</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:#8250DF;--shiki-dark:#D2A8FF\"> setWatched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.checked)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">// A select is controlled through value on the &#x3C;select>.</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">select</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\">genre</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\">(</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:#8250DF;--shiki-dark:#D2A8FF\"> setGenre</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.value)</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\">option</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"drama\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Drama&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</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\">option</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sci-fi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Sci-fi&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</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\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>حالة الأرقام توقع الجميع مرة على الأقل: <code dir=\"ltr\">e.target.value</code> نص دائمًا، حتى مع <code dir=\"ltr\">type=&quot;number&quot;</code>، فيعطي <code dir=\"ltr\">year + 1</code> النتيجة “20161”. والاحتفاظ بالنص الخام في الـ state يعني أيضًا أن الحقل الفارغ يبقى فارغًا بدل أن يتحوّل إلى <code dir=\"ltr\">0</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">React 19: الـ form actions</h2>\n<p>الـ controlled inputs هي الأفضل حين تتفاعل مع كل ضغطة مفتاح. لكن نماذج كثيرة لا تهمّ إلا لحظة الإرسال، وقد أضاف React 19 دعمًا مباشرًا لها: مرّر <strong>دالة</strong> إلى الخاصية <code dir=\"ltr\">action</code> في النموذج.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"3\"><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\"> QuickAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onAdd</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\"> addAction</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">formData</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    onAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(formData.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'title'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</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\">form</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> action</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">addAction</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\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"title\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> required</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\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add&#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\">form</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>تمنع React إعادة تحميل الصفحة نيابةً عنك، وتستدعي دالتك مع كائن <code dir=\"ltr\">FormData</code> مبني من خصائص <code dir=\"ltr\">name</code> في الحقول، وتعيد ضبط الحقول غير المتحكَّم بها حين تنتهي الـ action. فالحقول ليست متحكَّمًا بها أصلًا.</p>\n<p>وحين تحتاج إلى نتيجة من الإرسال، مثل رسالة خطأ، أو إلى حالة انتظار أثناء عمل غير متزامن، أضف <code dir=\"ltr\">useActionState</code>:</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useActionState } </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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> QuickAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onAdd</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\">error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">formAction</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isPending</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useActionState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">prevError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">formData</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\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> formData.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'title'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</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\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">title) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Title is required'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onAdd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(title); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// could be a request to a server</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</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\">  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\">form</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> action</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">formAction</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\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"title\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-invalid</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">error </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:#0550AE;--shiki-dark:#79C0FF\"> undefined</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\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">isPending</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\">isPending </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Adding…'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Add'</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\">button</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\">error </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;&#x26;</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\"> role</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"alert\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">error</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\">p</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\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">form</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>تستقبل الـ action الـ state السابق وبيانات النموذج، وتُرجع الـ state التالي. وتكون <code dir=\"ltr\">isPending</code> قيمتها <code dir=\"ltr\">true</code> أثناء تنفيذها. ويستطيع أي component ابن داخل النموذج أيضًا قراءة حالة الانتظار باستخدام <code dir=\"ltr\">useFormStatus</code> من <code dir=\"ltr\">react-dom</code>.</p>\n<p>أيّهما تستخدم؟ خيار افتراضي معقول: الـ controlled inputs حين تتفاعل الواجهة أثناء الكتابة (العدّ، والتحقّق المباشر، والحقول المعتمدة على بعضها)؛ والـ actions حين يحدث العمل عند الإرسال، خصوصًا إن كان غير متزامن. نموذج الإضافة في Watchlist يعطّل زرّه ما دام العنوان فارغًا، لذا يستخدم controlled input، وهذا ما ستبنيه في التمرين.</p>\n<aside class=\"callout callout-why\" 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\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا لا نقرأ الـ DOM فقط؟</span></p>\n<p>يمكنك أخذ القيمة عبر <code dir=\"ltr\">document.getElementById('title').value</code> عند الإرسال. ينجح هذا حتى تحتاج القيمة في أي مكان آخر: حالة تعطيل، أو معاينة، أو إعادة ضبط. أمّا الـ state فيعطيك مكانًا واحدًا تعيش فيه القيمة الحالية، ويمكن لكل جزء من الواجهة أن يعتمد عليه.</p>\n</aside>\n<p>في الدرس التالي تربط هذا النموذج بالقائمة. النموذج والقائمة أخوان، لذا يجب أن تعيش الأفلام في أبيهما المشترك. وهذا هو رفع الـ state للأعلى.</p>\n","toc":[{"id":"sec-1","text":"الـ controlled input","level":2},{"id":"sec-2","text":"الإرسال","level":2},{"id":"sec-3","text":"أنواع أخرى من الحقول","level":2},{"id":"sec-4","text":"React 19: الـ form actions","level":2}],"takeaways":["الـ controlled input يأخذ <code dir=\"ltr\">value</code> من الـ state ويبلّغ عن كل ضغطة مفتاح عبر <code dir=\"ltr\">onChange</code>، فيكون الـ state المصدر الوحيد للحقيقة.","تعامل مع <code dir=\"ltr\">onSubmit</code> على الـ <code dir=\"ltr\">&lt;form&gt;</code> واستدعِ <code dir=\"ltr\">e.preventDefault()</code>؛ فتحصل على الإرسال بـ Enter وعلى سلوك يدعم إمكانية الوصول مجانًا.","تحقّق من المدخلات انطلاقًا من الـ state أثناء الـ render، مثلًا بتعطيل الزر حين يكون العنوان بعد التشذيب فارغًا.","قيم الحقول نصوص دائمًا؛ حوّل الأرقام بنفسك، واستخدم <code dir=\"ltr\">checked</code> لمربّعات الاختيار.","تمرّر الـ form actions في React 19 كائن <code dir=\"ltr\">FormData</code> إلى دالة وتعيد ضبط النموذج؛ ويضيف <code dir=\"ltr\">useActionState</code> نتيجة ومؤشّر انتظار."],"further":[{"title":"Reacting to Input with State","url":"https://react.dev/learn/reacting-to-input-with-state"},{"title":"<input> reference (controlled inputs)","url":"https://react.dev/reference/react-dom/components/input"},{"title":"<form> reference (form actions)","url":"https://react.dev/reference/react-dom/components/form"},{"title":"useActionState reference","url":"https://react.dev/reference/react/useActionState"}],"quiz":[{"q":"<p>ترسم <code dir=\"ltr\">&lt;input value={title} /&gt;</code> من دون <code dir=\"ltr\">onChange</code>. ماذا يحدث حين يكتب المستخدم؟</p>","options":[{"text":"يتحدّث الحقل ويتحدّث الـ state المسمّى <code dir=\"ltr\">title</code> تلقائيًا.","why":"لا تحدّث React الـ state نيابةً عنك أبدًا. الـ controlled input لا يتغيّر إلا حين تضبط الـ state الذي يقرأ منه."},{"text":"لا يتغيّر شيء، وتحذّر React من أنك وفّرت <code dir=\"ltr\">value</code> من دون معالج <code dir=\"ltr\">onChange</code>.","why":"صحيح. كل render يعيد الحقل إلى <code dir=\"ltr\">title</code>. أضف <code dir=\"ltr\">onChange={(e) =&gt; setTitle(e.target.value)}</code>، أو استخدم <code dir=\"ltr\">defaultValue</code> لحقل غير متحكَّم به."},{"text":"يتحدّث الحقل، لكن <code dir=\"ltr\">title</code> يبقى كما هو.","why":"هكذا يتصرّف الحقل غير المتحكَّم به مع <code dir=\"ltr\">defaultValue</code>. أمّا مع <code dir=\"ltr\">value</code> فتُبقي React الـ DOM متوافقًا مع الـ state."},{"text":"ترمي React خطأ وتتوقّف عن الـ render.","why":"إنه تحذير في وضع التطوير، لا خطأ. كل ما في الأمر أن الحقل يصبح للقراءة فقط."}],"answer":1},{"q":"<p>حقل السنة هو <code dir=\"ltr\">&lt;input type=&quot;number&quot; value={year} onChange={(e) =&gt; setYear(e.target.value)} /&gt;</code>. كتب المستخدم 2016. ما نوع <code dir=\"ltr\">year</code>، وماذا يعطي <code dir=\"ltr\">year + 1</code>؟</p>","options":[{"text":"الرقم 2016، فيكون <code dir=\"ltr\">year + 1</code> هو 2017.","why":"الخاصية <code dir=\"ltr\">type=&quot;number&quot;</code> لا تغيّر إلا لوحة المفاتيح والتحقّق. أمّا <code dir=\"ltr\">e.target.value</code> فهي نص دائمًا."},{"text":"الرقم 2016، لكن <code dir=\"ltr\">year + 1</code> يعطي NaN.","why":"الرقم زائد 1 لا يكون NaN أبدًا. القيمة ليست رقمًا أصلًا."},{"text":"<code dir=\"ltr\">undefined</code>، لأن حقول الأرقام تحتاج إلى <code dir=\"ltr\">valueAsNumber</code>.","why":"الخاصية <code dir=\"ltr\">e.target.value</code> تعمل مع حقول الأرقام؛ لكنها تُرجع نصًا. و<code dir=\"ltr\">valueAsNumber</code> بديل يُرجع رقمًا."},{"text":"النص “2016”، فيكون <code dir=\"ltr\">year + 1</code> هو “20161”.","why":"صحيح. حوّل القيمة حين تستخدمها، مثلًا بـ <code dir=\"ltr\">Number(year)</code>، واحتفظ بالنص الخام في الـ state حتى تعمل المدخلات الجزئية كالحقل الفارغ."}],"answer":3},{"q":"<p>يضيف نموذجك فيلمًا ويجب أن يمسح العنوان بعد ذلك. أيّ <code dir=\"ltr\">handleSubmit</code> صحيح مع controlled input؟</p>","options":[{"text":"<code dir=\"ltr\">e.preventDefault(); onAdd(title.trim()); setTitle('');</code>","why":"صحيح. توقف preventDefault إعادة تحميل الصفحة، وتبلّغ onAdd عن الفيلم الجديد، وضبط الـ state على ‘’ يمسح الـ controlled input."},{"text":"<code dir=\"ltr\">onAdd(title); e.target.reset();</code>","why":"من دون preventDefault يحاول المتصفّح الإرسال وإعادة التحميل. كما أن reset() لا تغيّر الـ state المسمّى <code dir=\"ltr\">title</code>، فتعيد React القيمة القديمة في الـ render التالي."},{"text":"<code dir=\"ltr\">e.preventDefault(); onAdd(title); title = '';</code>","why":"إعادة إسناد المتغيّر لا تفعل شيئًا بالـ state؛ يظل الحقل يعرض العنوان القديم."}],"answer":0},{"q":"<p>متى تكون الـ form action في React 19 مع <code dir=\"ltr\">useActionState</code> أنسب من الـ controlled inputs؟</p>","options":[{"text":"حين تحتاج إلى تغيير ما يُعرض مع كل ضغطة مفتاح، مثل العدّ المباشر للحروف.","why":"الملاحظات المباشرة تحتاج إلى القيمة في الـ state أثناء الكتابة، وهذا ما تعطيك إياه الـ controlled inputs."},{"text":"أبدًا؛ الـ actions لا تعمل إلا مع أطر عمل الخادم مثل Next.js.","why":"تعمل الـ form actions مع دوال عادية في المتصفّح داخل أي تطبيق React 19. والـ Server Functions استخدام اختياري واحد منها."},{"text":"حين يهمّ النموذج أساسًا لحظة الإرسال، خصوصًا مع عمل غير متزامن، وتريد نتيجة أو خطأ مع مؤشّر انتظار.","why":"صحيح. تستقبل الـ action كائن FormData، ويخزّن <code dir=\"ltr\">useActionState</code> ما تُرجعه، وتكون <code dir=\"ltr\">isPending</code> قيمتها true أثناء تنفيذها."},{"text":"حين يحتوي النموذج على حقل واحد فقط.","why":"عدد الحقول لا يحسم الأمر. المهمّ هو هل تتفاعل مع الكتابة أم مع الإرسال."}],"answer":2}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useState } from 'react';\n\nfunction AddMovieForm({ onAdd }) {\n  // Make both inputs controlled, handle submit, and disable Add while the title is blank.\n  return (\n    <form>\n      <label htmlFor=\"title\">Title</label>\n      <input id=\"title\" />\n      <label htmlFor=\"year\">Year</label>\n      <input id=\"year\" type=\"number\" />\n      <button type=\"submit\">Add</button>\n    </form>\n  );\n}\n\nlet nextId = 1; // in a real app: crypto.randomUUID()\n\nexport default function App() {\n  const [movies, setMovies] = useState([]);\n\n  function addMovie(title, year) {\n    setMovies([...movies, { id: nextId++, title, year }]);\n  }\n\n  return (\n    <main>\n      <AddMovieForm onAdd={addMovie} />\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>{m.year ? `${m.title} (${m.year})` : m.title}</li>\n        ))}\n      </ul>\n    </main>\n  );\n}\n"},"solution":{"jsx":"import { useState } from 'react';\n\nfunction AddMovieForm({ onAdd }) {\n  const [title, setTitle] = useState('');\n  const [year, setYear] = useState('');\n  const trimmed = title.trim();\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    if (!trimmed) return;\n    onAdd(trimmed, year === '' ? null : Number(year));\n    setTitle('');\n    setYear('');\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <label htmlFor=\"title\">Title</label>\n      <input id=\"title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n      <label htmlFor=\"year\">Year</label>\n      <input id=\"year\" type=\"number\" value={year} onChange={(e) => setYear(e.target.value)} />\n      <button type=\"submit\" disabled={!trimmed}>Add</button>\n    </form>\n  );\n}\n\nlet nextId = 1; // in a real app: crypto.randomUUID()\n\nexport default function App() {\n  const [movies, setMovies] = useState([]);\n\n  function addMovie(title, year) {\n    setMovies([...movies, { id: nextId++, title, year }]);\n  }\n\n  return (\n    <main>\n      <AddMovieForm onAdd={addMovie} />\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>{m.year ? `${m.title} (${m.year})` : m.title}</li>\n        ))}\n      </ul>\n    </main>\n  );\n}\n"},"checks":[{"id":"disabled-empty","test":"const b = $('button'); if (!b.disabled) return false; await input('#title', '   '); return $('button').disabled === true;"},{"id":"enabled-with-title","test":"await input('#title', 'Dune'); return $('button').disabled === false;"},{"id":"adds-movie","test":"await input('#year', '2021'); const __f = $('form'); if (__f) __f.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); else await click($('button')); await tick(20); const li = $$('li'); return li.length === 1 && text(li[0]) === 'Dune (2021)';"},{"id":"clears-fields","test":"return $('#title').value === '' && $('#year').value === '' && $('button').disabled === true;"},{"id":"trims-title","test":"await input('#title', '  Past Lives  '); const __f = $('form'); if (__f) __f.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); else await click($('button')); await tick(20); const li = $$('li'); return li.length === 2 && text(li[1]) === 'Past Lives';"}],"title":"نموذج إضافة الأفلام","prompt":"<p>ابنِ <code dir=\"ltr\">AddMovieForm</code> بحيث يضيف أفلامًا إلى القائمة. أبقِ المعرّفين <code dir=\"ltr\">title</code> و<code dir=\"ltr\">year</code> على الحقلين.</p>\n<ul>\n<li>اجعل الحقلين كليهما <strong>controlled</strong> (<code dir=\"ltr\">value</code> + <code dir=\"ltr\">onChange</code>).</li>\n<li>عند الإرسال، استدعِ <code dir=\"ltr\">onAdd(title, year)</code> بالعنوان <strong>بعد التشذيب</strong> وبالسنة كرقم، أو <code dir=\"ltr\">null</code> حين يكون حقل السنة فارغًا. لا تدع الصفحة تُعاد تحميلها.</li>\n<li>بعد الإضافة، <strong>امسح الحقلين كليهما</strong>.</li>\n<li>يكون زر <strong>Add</strong> معطّلًا (<code dir=\"ltr\">disabled</code>) ما دام العنوان بعد التشذيب فارغًا.</li>\n</ul>\n<p>يخزّن <code dir=\"ltr\">App</code> الأفلام مسبقًا ويعرضها بالشكل “Title (Year)”، أو “Title” وحده حين لا توجد سنة.</p>\n","hints":["قطعتان من الـ state، تبدأ كلتاهما بـ <code dir=\"ltr\">''</code>: <code dir=\"ltr\">const [title, setTitle] = useState('')</code> والمثل لـ <code dir=\"ltr\">year</code>.","ضع <code dir=\"ltr\">onSubmit={handleSubmit}</code> على الـ <code dir=\"ltr\">&lt;form&gt;</code> وابدأ المعالج بـ <code dir=\"ltr\">e.preventDefault()</code>.","اشتقّ <code dir=\"ltr\">const trimmed = title.trim();</code> واستخدمها لكلٍّ من <code dir=\"ltr\">disabled={!trimmed}</code> والقيمة التي تمرّرها إلى <code dir=\"ltr\">onAdd</code>. وحوّل السنة بـ <code dir=\"ltr\">year === '' ? null : Number(year)</code>."],"explanation":"<p>مع الـ controlled inputs تصبح القيم الحالية للنموذج state عاديًا، وهذا ما يجعل البقية سهلة. يُعاد حساب <code dir=\"ltr\">disabled={!trimmed}</code> مع كل ضغطة مفتاح، ومسح النموذج هو <code dir=\"ltr\">setTitle('')</code> و<code dir=\"ltr\">setYear('')</code>، والمعالج يقرأ القيم مباشرة من الـ state. وتوقف <code dir=\"ltr\">e.preventDefault()</code> إرسال المتصفّح للصفحة كاملة، وتحويل السنة داخل المعالج يُبقي النص الخام في الـ state، فيبقى الحقل الفارغ فارغًا بدل أن يصبح 0.</p>\n<p><em>ملاحظة عن ساحة التجربة: ترسل الفحوص النموذج بإطلاق حدث <code dir=\"ltr\">submit</code> الخاص به مباشرة.</em></p>\n","checkLabels":{"disabled-empty":"زر Add معطّل حين يكون العنوان فارغًا أو مسافات فقط","enabled-with-title":"يصبح زر Add مفعّلًا بمجرّد كتابة عنوان","adds-movie":"الإرسال يضيف “Dune (2021)” إلى القائمة","clears-fields":"يُمسح الحقلان كلاهما بعد الإضافة","trims-title":"يُشذَّب العنوان المحاط بمسافات، والسنة الفارغة تعرض العنوان وحده"}},"words":671,"readMinutes":4}