{"id":"l-rf-3-3","courseId":"react-fundamentals","lang":"en","kind":"lesson","summary":"Build the Watchlist’s add-movie form with controlled inputs, handle submission and validation, and know when React 19 form actions and useActionState are the better fit.","html":"<p>Every useful app takes input. For the Watchlist that means a form: type a title, optionally a year, press Enter or click Add, and the movie appears in the list. Forms are where state and events meet, and where most beginner bugs live: inputs that won’t accept typing, pages that reload on submit, numbers that concatenate like strings. Each one has a clear cause.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A controlled input</h2>\n<p>In plain HTML the input owns its value; you read it when you need it. In React the usual approach flips that: <strong>state owns the value</strong>, and the input displays it.</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=\"Copy code\"><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>Copy</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>This is a <strong>controlled input</strong>. Every keystroke fires <code dir=\"ltr\">onChange</code>, the handler stores the new text in state, React re-renders, and the input shows the state. It sounds roundabout, but now the current text is an ordinary variable. You can show a character count, validate as the user types, disable a button, or clear the field by setting state to <code dir=\"ltr\">''</code>.</p>\n<p>Note <code dir=\"ltr\">onChange</code> in React fires on every keystroke, like the DOM’s <code dir=\"ltr\">input</code> event, not only when the field loses focus.</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\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span><code dir=\"ltr\">value</code> without <code dir=\"ltr\">onChange</code></span></p>\n<p><code dir=\"ltr\">&lt;input value={title} /&gt;</code> on its own makes a read-only field: each render puts <code dir=\"ltr\">title</code> back, so typing does nothing, and React warns in the console. Either add <code dir=\"ltr\">onChange</code> or, if you don’t need the value in state, use <code dir=\"ltr\">defaultValue</code> to make it uncontrolled. A related warning, “changing an uncontrolled input to be controlled”, means <code dir=\"ltr\">value</code> started as <code dir=\"ltr\">undefined</code>; initialise state with <code dir=\"ltr\">''</code>.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Submitting</h2>\n<p>Handle submission on the form, not on the button’s click:</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=\"Copy code\"><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>Copy</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> on the form means pressing Enter in the input works, the button works, and assistive technology understands the form, all without extra code. <code dir=\"ltr\">e.preventDefault()</code> stops the browser’s default, which is to send the form to a URL and reload the page. The component reports the new title through <code dir=\"ltr\">onAdd</code> and clears the field by resetting state.</p>\n<p>Validation comes from state too. <code dir=\"ltr\">trimmed</code> is derived on every render, so the button is disabled exactly while the title is blank, including a title of only spaces. The <code dir=\"ltr\">if (!trimmed) return;</code> inside the handler is a second guard in case the form is submitted another way.</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\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Show errors next to the field</span></p>\n<p>When you display a message such as “Title is required”, connect it to the input with <code dir=\"ltr\">aria-describedby</code> pointing at the message’s <code dir=\"ltr\">id</code>, and set <code dir=\"ltr\">aria-invalid={true}</code> on the field. Screen readers then announce the error when the input gets focus.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Other kinds of input</h2>\n<p>Every input type follows the same pattern with small differences:</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=\"Copy code\"><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>Copy</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>The number case catches everyone once: <code dir=\"ltr\">e.target.value</code> is always a string, even for <code dir=\"ltr\">type=&quot;number&quot;</code>, so <code dir=\"ltr\">year + 1</code> gives “20161”. Keeping the raw string in state also means an empty field stays empty instead of turning into <code dir=\"ltr\">0</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">React 19: form actions</h2>\n<p>Controlled inputs are best when you react to every keystroke. Many forms only matter at the moment of submission, and React 19 added first-class support for those: pass a <strong>function</strong> to the form’s <code dir=\"ltr\">action</code> prop.</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=\"Copy code\"><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>Copy</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 prevents the page reload for you, calls your function with a <code dir=\"ltr\">FormData</code> object built from the fields’ <code dir=\"ltr\">name</code> attributes, and resets the form’s uncontrolled fields when the action finishes. The fields aren’t controlled at all.</p>\n<p>When you need a result from the submission, such as an error message, or a pending state while async work runs, add <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=\"Copy code\"><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>Copy</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>The action receives the previous state and the form data and returns the next state. <code dir=\"ltr\">isPending</code> is <code dir=\"ltr\">true</code> while it runs. A child component inside the form can also read the pending status with <code dir=\"ltr\">useFormStatus</code> from <code dir=\"ltr\">react-dom</code>.</p>\n<p>Which should you use? A sensible default: controlled inputs when the UI reacts as the user types (counts, live validation, dependent fields); actions when the work happens on submit, especially if it’s async. The Watchlist’s add form disables its button while the title is blank, so it uses a controlled input, and that’s what you’ll build in the exercise.</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\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why not just read the DOM?</span></p>\n<p>You could grab the value with <code dir=\"ltr\">document.getElementById('title').value</code> on submit. It works until you need the value anywhere else: a disabled state, a preview, a reset. State gives you one place where the current value lives, and every part of the UI can depend on it.</p>\n</aside>\n<p>Next, you’ll connect this form to the list. The form and the list are siblings, so the movies have to live in their common parent. That’s lifting state up.</p>\n","toc":[{"id":"sec-1","text":"A controlled input","level":2},{"id":"sec-2","text":"Submitting","level":2},{"id":"sec-3","text":"Other kinds of input","level":2},{"id":"sec-4","text":"React 19: form actions","level":2}],"takeaways":["A controlled input gets its <code dir=\"ltr\">value</code> from state and reports every keystroke through <code dir=\"ltr\">onChange</code>, so state is the single source of truth.","Handle <code dir=\"ltr\">onSubmit</code> on the <code dir=\"ltr\">&lt;form&gt;</code> and call <code dir=\"ltr\">e.preventDefault()</code>; you get Enter-to-submit and accessible behaviour for free.","Validate from state while rendering, for example by disabling the button when the trimmed title is empty.","Input values are always strings; convert numbers yourself and use <code dir=\"ltr\">checked</code> for checkboxes.","React 19 form actions pass <code dir=\"ltr\">FormData</code> to a function and reset the form; <code dir=\"ltr\">useActionState</code> adds a result and a pending flag."],"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>You render <code dir=\"ltr\">&lt;input value={title} /&gt;</code> with no <code dir=\"ltr\">onChange</code>. What happens when the user types?</p>","options":[{"text":"The input updates and <code dir=\"ltr\">title</code> state updates automatically.","why":"React never updates your state for you. A controlled input only changes when you set the state it reads from."},{"text":"Nothing changes, and React warns that you provided <code dir=\"ltr\">value</code> without an <code dir=\"ltr\">onChange</code> handler.","why":"Correct. Every render forces the input back to <code dir=\"ltr\">title</code>. Add <code dir=\"ltr\">onChange={(e) =&gt; setTitle(e.target.value)}</code>, or use <code dir=\"ltr\">defaultValue</code> for an uncontrolled field."},{"text":"The input updates, but <code dir=\"ltr\">title</code> stays the same.","why":"That’s how an uncontrolled input with <code dir=\"ltr\">defaultValue</code> behaves. With <code dir=\"ltr\">value</code>, React keeps the DOM in line with state."},{"text":"React throws an error and stops rendering.","why":"It’s a development warning, not an error. The field just becomes read-only."}],"answer":1},{"q":"<p>The year field is <code dir=\"ltr\">&lt;input type=&quot;number&quot; value={year} onChange={(e) =&gt; setYear(e.target.value)} /&gt;</code>. The user types 2016. What type is <code dir=\"ltr\">year</code>, and what does <code dir=\"ltr\">year + 1</code> give?</p>","options":[{"text":"The number 2016, so <code dir=\"ltr\">year + 1</code> is 2017.","why":"<code dir=\"ltr\">type=&quot;number&quot;</code> only changes the keyboard and validation. <code dir=\"ltr\">e.target.value</code> is always a string."},{"text":"The number 2016, but <code dir=\"ltr\">year + 1</code> is NaN.","why":"A number plus 1 is never NaN. The value isn’t a number in the first place."},{"text":"<code dir=\"ltr\">undefined</code>, because number inputs need <code dir=\"ltr\">valueAsNumber</code>.","why":"<code dir=\"ltr\">e.target.value</code> works on number inputs; it just returns a string. <code dir=\"ltr\">valueAsNumber</code> is an alternative that returns a number."},{"text":"The string “2016”, so <code dir=\"ltr\">year + 1</code> is “20161”.","why":"Correct. Convert when you use it, for example with <code dir=\"ltr\">Number(year)</code>, and keep the raw string in state so partial input like an empty field works."}],"answer":3},{"q":"<p>Your form adds a movie and should clear the title afterwards. Which <code dir=\"ltr\">handleSubmit</code> is right for a controlled input?</p>","options":[{"text":"<code dir=\"ltr\">e.preventDefault(); onAdd(title.trim()); setTitle('');</code>","why":"Correct. preventDefault stops the page reload, onAdd reports the new movie, and setting the state to ‘’ clears the controlled input."},{"text":"<code dir=\"ltr\">onAdd(title); e.target.reset();</code>","why":"Without preventDefault the browser tries to submit and reload. And reset() doesn’t change <code dir=\"ltr\">title</code> state, so React puts the old value back on the next render."},{"text":"<code dir=\"ltr\">e.preventDefault(); onAdd(title); title = '';</code>","why":"Reassigning the variable does nothing to state; the field keeps showing the old title."}],"answer":0},{"q":"<p>When is a React 19 form action with <code dir=\"ltr\">useActionState</code> a better fit than controlled inputs?</p>","options":[{"text":"When you need to change what’s shown on every keystroke, such as live character counts.","why":"Live feedback needs the value in state as the user types, which is what controlled inputs give you."},{"text":"Never; actions only work with server frameworks like Next.js.","why":"Form actions work with plain client-side functions in any React 19 app. Server Functions are one optional use."},{"text":"When the form mainly matters at submit time, especially with async work, and you want a result or error plus a pending flag.","why":"Correct. The action receives FormData, <code dir=\"ltr\">useActionState</code> stores what it returns, and <code dir=\"ltr\">isPending</code> is true while it runs."},{"text":"When the form has only one field.","why":"The number of fields doesn’t decide it. What matters is whether you react to typing or to submitting."}],"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":"The add-movie form","prompt":"<p>Build <code dir=\"ltr\">AddMovieForm</code> so it adds movies to the list. Keep the <code dir=\"ltr\">id</code>s <code dir=\"ltr\">title</code> and <code dir=\"ltr\">year</code> on the inputs.</p>\n<ul>\n<li>Make both inputs <strong>controlled</strong> (<code dir=\"ltr\">value</code> + <code dir=\"ltr\">onChange</code>).</li>\n<li>On submit, call <code dir=\"ltr\">onAdd(title, year)</code> with the <strong>trimmed</strong> title and the year as a number, or <code dir=\"ltr\">null</code> when the year field is empty. Don’t let the page reload.</li>\n<li>After adding, <strong>clear both fields</strong>.</li>\n<li>The <strong>Add</strong> button is <code dir=\"ltr\">disabled</code> while the trimmed title is empty.</li>\n</ul>\n<p><code dir=\"ltr\">App</code> already stores the movies and renders them as “Title (Year)”, or just “Title” when there’s no year.</p>\n","hints":["Two pieces of state, both starting as <code dir=\"ltr\">''</code>: <code dir=\"ltr\">const [title, setTitle] = useState('')</code> and the same for <code dir=\"ltr\">year</code>.","Put <code dir=\"ltr\">onSubmit={handleSubmit}</code> on the <code dir=\"ltr\">&lt;form&gt;</code> and start the handler with <code dir=\"ltr\">e.preventDefault()</code>.","Derive <code dir=\"ltr\">const trimmed = title.trim();</code> and use it for both <code dir=\"ltr\">disabled={!trimmed}</code> and the value you pass to <code dir=\"ltr\">onAdd</code>. Convert the year with <code dir=\"ltr\">year === '' ? null : Number(year)</code>."],"explanation":"<p>With controlled inputs, the form’s current values are ordinary state, which is what makes the rest easy. <code dir=\"ltr\">disabled={!trimmed}</code> is recalculated on every keystroke, clearing the form is <code dir=\"ltr\">setTitle('')</code> and <code dir=\"ltr\">setYear('')</code>, and the handler reads the values straight from state. <code dir=\"ltr\">e.preventDefault()</code> stops the browser’s full-page submit, and converting the year in the handler keeps the raw string in state so an empty field stays empty instead of becoming 0.</p>\n<p><em>Playground note: the checks submit the form by firing its <code dir=\"ltr\">submit</code> event directly.</em></p>\n","checkLabels":{"disabled-empty":"Add is disabled when the title is empty or only spaces","enabled-with-title":"Add is enabled once a title is typed","adds-movie":"Submitting adds “Dune (2021)” to the list","clears-fields":"Both fields are cleared after adding","trims-title":"A padded title is trimmed, and an empty year shows the title alone"}},"words":742,"readMinutes":4}