{"id":328,"date":"2026-06-15T13:05:09","date_gmt":"2026-06-15T13:05:09","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/react-19-changed-how-i-write-forms\/"},"modified":"2026-06-15T13:05:09","modified_gmt":"2026-06-15T13:05:09","slug":"react-19-changed-how-i-write-forms","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/react-19-changed-how-i-write-forms\/","title":{"rendered":"React 19 Changed How I Write Forms (For the Better)"},"content":{"rendered":"<p>I had a <code>&lt;form&gt;<\/code> in a side project that I rewrote four times last year, and every version was a little worse than the one before it. Loading flag here, error state there, a <code>useEffect<\/code> to reset the input after a successful submit, a ref so I could focus the field again. None of it was hard. All of it was annoying. By the fourth pass I had a 60-line component that did roughly what a plain HTML form did in 1998, except mine could also get stuck in a spinner if the network hiccuped.<\/p>\n<p>Then I moved the project to React 19 and deleted about half of it. This isn&rsquo;t a &ldquo;everything changed&rdquo; post, because most of my React looks the same. But forms, optimistic updates, and reading async data got genuinely simpler, and I want to show you the before-and-after on the parts I actually use.<\/p>\n<h2 id=\"the-thing-i-kept-getting-wrong-in-react-18\">The thing I kept getting wrong in React 18<\/h2>\n<p>Here&rsquo;s the shape of the problem. In React 18, a submit handler that talks to the network needs you to track three things by hand: whether it&rsquo;s pending, whether it failed, and what to do after it succeeds. Miss one and the UI lies to the user.<\/p>\n<pre><code class=\"language-jsx\">\/\/ React 18\nfunction CommentForm({ postId }) {\n  const [pending, setPending] = useState(false);\n  const [error, setError] = useState(null);\n  const inputRef = useRef(null);\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setPending(true);\n    setError(null);\n    try {\n      await postComment(postId, inputRef.current.value);\n      inputRef.current.value = &quot;&quot;;\n    } catch (err) {\n      setError(err.message);\n    } finally {\n      setPending(false);\n    }\n  }\n\n  return (\n    &lt;form onSubmit={handleSubmit}&gt;\n      &lt;input ref={inputRef} name=&quot;comment&quot; disabled={pending} \/&gt;\n      &lt;button disabled={pending}&gt;{pending ? &quot;Posting...&quot; : &quot;Post&quot;}&lt;\/button&gt;\n      {error &amp;&amp; &lt;p className=&quot;err&quot;&gt;{error}&lt;\/p&gt;}\n    &lt;\/form&gt;\n  );\n}\n<\/code><\/pre>\n<p>It works. I shipped versions of this for years. But the bookkeeping is all mine, and the bugs live in the gaps: forgetting to clear the error on retry, leaving the button disabled after an exception you didn&rsquo;t expect, racing two submits because nothing stops the second one.<\/p>\n<h2 id=\"useactionstate-the-form-state-i-stopped-wiring-by-hand\">useActionState: the form state I stopped wiring by hand<\/h2>\n<p>React 19 ships an <code>actions<\/code> model and a hook called <code>useActionState<\/code> that owns the pending and result state for you. You give it an async function; it gives you back the latest state, a wrapped action, and a pending boolean.<\/p>\n<pre><code class=\"language-jsx\">\/\/ React 19\nimport { useActionState } from &quot;react&quot;;\n\nfunction CommentForm({ postId }) {\n  const [error, submitAction, pending] = useActionState(\n    async (_prev, formData) =&gt; {\n      try {\n        await postComment(postId, formData.get(&quot;comment&quot;));\n        return null; \/\/ no error\n      } catch (err) {\n        return err.message;\n      }\n    },\n    null\n  );\n\n  return (\n    &lt;form action={submitAction}&gt;\n      &lt;input name=&quot;comment&quot; disabled={pending} \/&gt;\n      &lt;button disabled={pending}&gt;{pending ? &quot;Posting...&quot; : &quot;Post&quot;}&lt;\/button&gt;\n      {error &amp;&amp; &lt;p className=&quot;err&quot;&gt;{error}&lt;\/p&gt;}\n    &lt;\/form&gt;\n  );\n}\n<\/code><\/pre>\n<p>No <code>useState<\/code> for pending. No <code>useRef<\/code>. The <code>&lt;form action={...}&gt;<\/code> prop is the part that surprised people: forms can now take a function directly, and React resets uncontrolled fields on a successful action so I stopped manually clearing the input. The official <a href=\"https:\/\/react.dev\/blog\/2024\/12\/05\/react-19\" rel=\"nofollow noopener\" target=\"_blank\">React 19 release notes<\/a> walk through the whole actions model, and the <a href=\"https:\/\/react.dev\/reference\/react\/useActionState\" rel=\"nofollow noopener\" target=\"_blank\"><code>useActionState<\/code> reference<\/a> is worth a read because the argument order trips people up the first time.<\/p>\n<p>The win isn&rsquo;t fewer lines, although it is fewer lines. The win is that pending state and the form are wired together by the framework, so the two can&rsquo;t drift apart. I covered a related cleanup in my post on <a href=\"https:\/\/abrarqasim.com\/blog\/react-19-forwardref-why-i-stopped-writing-it\" rel=\"noopener\">why I stopped writing forwardRef<\/a>, and it&rsquo;s the same theme: React 19 quietly absorbed a chunk of plumbing I used to own.<\/p>\n<h2 id=\"useoptimistic-or-how-i-stopped-faking-responsiveness-badly\">useOptimistic, or how I stopped faking responsiveness badly<\/h2>\n<p>The other thing I always hand-rolled was optimistic UI. Show the comment immediately, then reconcile when the server answers. My old approach kept a shadow copy of the list in state and a mess of logic to roll it back on failure. It worked until it didn&rsquo;t, usually when two actions overlapped.<\/p>\n<p><code>useOptimistic<\/code> gives you a temporary state that automatically reverts when the surrounding action finishes or throws.<\/p>\n<pre><code class=\"language-jsx\">\/\/ React 19\nimport { useOptimistic } from &quot;react&quot;;\n\nfunction Comments({ comments, postId }) {\n  const [optimistic, addOptimistic] = useOptimistic(\n    comments,\n    (state, newText) =&gt; [...state, { id: &quot;temp&quot;, text: newText, sending: true }]\n  );\n\n  async function action(formData) {\n    const text = formData.get(&quot;comment&quot;);\n    addOptimistic(text);\n    await postComment(postId, text);\n  }\n\n  return (\n    &lt;&gt;\n      &lt;ul&gt;{optimistic.map(c =&gt; (\n        &lt;li key={c.id} style={{ opacity: c.sending ? 0.5 : 1 }}&gt;{c.text}&lt;\/li&gt;\n      ))}&lt;\/ul&gt;\n      &lt;form action={action}&gt;&lt;input name=&quot;comment&quot; \/&gt;&lt;button&gt;Post&lt;\/button&gt;&lt;\/form&gt;\n    &lt;\/&gt;\n  );\n}\n<\/code><\/pre>\n<p>When the action resolves, the optimistic entry drops and the real list (refreshed however you refresh it) takes over. When it throws, React rolls the optimistic state back for you. I wrote a longer field report on this in <a href=\"https:\/\/abrarqasim.com\/blog\/optimistic-ui-react-19-useoptimistic-six-months-in\" rel=\"noopener\">six months with useOptimistic<\/a>, including the cases where it bit me, so I won&rsquo;t repeat all of it here.<\/p>\n<h2 id=\"the-use-hook-and-reading-promises-without-useeffect\">The <code>use<\/code> hook and reading promises without useEffect<\/h2>\n<p><code>use<\/code> is the one I&rsquo;m still careful with. It lets a component read a promise (or context) during render, and it suspends until the promise resolves. Paired with a Suspense boundary, it replaces a lot of the <code>useEffect<\/code> plus loading-flag dance for data you pass down.<\/p>\n<pre><code class=\"language-jsx\">\/\/ React 19\nimport { use, Suspense } from &quot;react&quot;;\n\nfunction Profile({ userPromise }) {\n  const user = use(userPromise); \/\/ suspends until resolved\n  return &lt;h1&gt;{user.name}&lt;\/h1&gt;;\n}\n\nfunction Page({ userPromise }) {\n  return (\n    &lt;Suspense fallback={&lt;p&gt;Loading...&lt;\/p&gt;}&gt;\n      &lt;Profile userPromise={userPromise} \/&gt;\n    &lt;\/Suspense&gt;\n  );\n}\n<\/code><\/pre>\n<p>The catch: you don&rsquo;t want to create that promise inside the component on every render, or you&rsquo;ll fetch in a loop. You create it higher up, or in a server component, and hand it down. The <a href=\"https:\/\/react.dev\/reference\/react\/use\" rel=\"nofollow noopener\" target=\"_blank\"><code>use<\/code> reference<\/a> is blunt about the rules, and I&rsquo;d read it twice before reaching for this in anything load-bearing. I got it wrong for about a week before the loop made sense to me.<\/p>\n<p>One more thing that <code>use<\/code> does, which is easy to miss: it can read context, and unlike <code>useContext<\/code>, you&rsquo;re allowed to call it conditionally. That sounds like a footnote until you have a component that only needs theme context on one branch and you&rsquo;ve been pulling it unconditionally at the top for years out of habit. Small thing. It adds up.<\/p>\n<h2 id=\"useformstatus-the-prop-drilling-i-stopped-doing\">useFormStatus: the prop drilling I stopped doing<\/h2>\n<p>There&rsquo;s a quieter hook that pairs with all of this: <code>useFormStatus<\/code>. It lets a child component read the pending state of the form it sits inside, without you threading a <code>pending<\/code> prop down through every layer. The classic case is a submit button that lives three components deep in some design-system wrapper.<\/p>\n<pre><code class=\"language-jsx\">\/\/ React 19\nimport { useFormStatus } from &quot;react-dom&quot;;\n\nfunction SubmitButton() {\n  const { pending } = useFormStatus();\n  return &lt;button disabled={pending}&gt;{pending ? &quot;Saving...&quot; : &quot;Save&quot;}&lt;\/button&gt;;\n}\n<\/code><\/pre>\n<p><code>SubmitButton<\/code> doesn&rsquo;t take any props about loading state. It reads it from the nearest parent <code>&lt;form&gt;<\/code>. The first time I deleted a <code>pending<\/code> prop that had been passed through four components, I actually said &ldquo;oh, thank god&rdquo; out loud. It&rsquo;s a small ergonomic win, but prop drilling boolean state is the kind of thing that quietly makes a codebase feel heavier than it is.<\/p>\n<p>The catch worth knowing: <code>useFormStatus<\/code> only reads the form it&rsquo;s a descendant of, and the button has to be inside that <code>&lt;form&gt;<\/code>, not rendering it. If you put the hook in the same component that renders the form, you get nothing back, because it&rsquo;s looking at a parent that isn&rsquo;t there. I tripped on that exactly once.<\/p>\n<h2 id=\"what-id-actually-adopt-first\">What I&rsquo;d actually adopt first<\/h2>\n<p>If you&rsquo;re upgrading an existing app, don&rsquo;t rewrite everything. Start with <code>useActionState<\/code> on your noisiest forms, the ones with the most hand-managed pending and error state. That&rsquo;s where the cleanup pays for itself immediately and the risk is low, because the behavior is the same and you&rsquo;re deleting code rather than adding it.<\/p>\n<p>Add <code>useOptimistic<\/code> only where you already fake responsiveness, and skip it where a plain spinner is honest enough. Leave <code>use<\/code> and Suspense-driven data fetching for new code or a deliberate refactor, because the promise-creation rule is easy to trip over.<\/p>\n<p>One caveat I&rsquo;d flag before you go all-in: these patterns assume you&rsquo;re comfortable with the actions model, and if your app leans heavily on a data library like TanStack Query or Redux Toolkit Query, there&rsquo;s some overlap to think through. The hooks don&rsquo;t replace those tools, and bolting both onto the same flow can muddy who owns the loading state. I&rsquo;d pick one source of truth per interaction rather than letting React&rsquo;s form actions and your query library both try to manage pending state. When they fight, you get flicker, and flicker is worse than a slightly more verbose component.<\/p>\n<p>A concrete thing to do this week: open the form in your app that has the most <code>useState<\/code> calls around submitting, and try porting just that one to <code>useActionState<\/code>. You&rsquo;ll know within an hour whether the trade feels right. If you want to see how I think about adopting framework features without betting the whole codebase on them, that&rsquo;s most of what I write about over on <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my work page<\/a>. React 19 didn&rsquo;t reinvent how I build. It just took back some chores I was tired of doing by hand.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>React 19 forms, useActionState, and useOptimistic with before-and-after code. The features I actually kept after upgrading, and the one I&#8217;m still careful with.<\/p>\n","protected":false},"author":2,"featured_media":327,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"React 19 forms, useActionState, and useOptimistic with before-and-after code. The features I actually kept after upgrading, and the one I'm still careful with.","rank_math_focus_keyword":"react 19","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[354,35],"tags":[38,44,41,359,172],"class_list":["post-328","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react","category-web-development","tag-frontend","tag-javascript","tag-react","tag-react-19-2","tag-web-development-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/328","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=328"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/328\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/327"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=328"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=328"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=328"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}