{"id":145,"date":"2026-04-24T05:02:45","date_gmt":"2026-04-24T05:02:45","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/react-19-features-i-actually-use-six-months-in\/"},"modified":"2026-04-24T05:02:45","modified_gmt":"2026-04-24T05:02:45","slug":"react-19-features-i-actually-use-six-months-in","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/react-19-features-i-actually-use-six-months-in\/","title":{"rendered":"React 19 features I actually use, six months in"},"content":{"rendered":"<p>Confession: when React 19 went stable, I rewrote half of my dashboard the same weekend. Then I rewrote about a third of it back, because most of what I changed didn&rsquo;t earn its keep. Six months in, I&rsquo;ve got opinions.<\/p>\n<p>This isn&rsquo;t a &ldquo;what&rsquo;s new&rdquo; rundown. The <a href=\"https:\/\/react.dev\/blog\/2024\/12\/05\/react-19\" rel=\"nofollow noopener\" target=\"_blank\">official React 19 release notes<\/a> already exist and they&rsquo;re fine. This post is about what survived contact with a real codebase. The parts I keep reaching for. A few I forget exist. And the ones I quietly reverted.<\/p>\n<h2 id=\"actions-and-useactionstate-replaced-my-form-reducers\">Actions and useActionState replaced my form reducers<\/h2>\n<p>The biggest day-to-day change for me is <code>useActionState<\/code> plus form Actions. I had a small mountain of <code>useReducer({ status, error, fieldErrors })<\/code> plumbing for forms. Most of it is gone now.<\/p>\n<p>Before, every form needed roughly this:<\/p>\n<pre><code class=\"language-jsx\">function ContactForm() {\n  const [state, dispatch] = useReducer(formReducer, initialState);\n  const [pending, setPending] = useState(false);\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setPending(true);\n    try {\n      await submitContact(new FormData(e.target));\n      dispatch({ type: 'success' });\n    } catch (err) {\n      dispatch({ type: 'error', error: err.message });\n    } finally {\n      setPending(false);\n    }\n  }\n  \/\/ ...\n}\n<\/code><\/pre>\n<p>After:<\/p>\n<pre><code class=\"language-jsx\">function ContactForm() {\n  const [state, formAction, pending] = useActionState(submitContact, { ok: false });\n  return (\n    &lt;form action={formAction}&gt;\n      {\/* fields *\/}\n      &lt;button disabled={pending}&gt;Send&lt;\/button&gt;\n      {state.error &amp;&amp; &lt;p role=&quot;alert&quot;&gt;{state.error}&lt;\/p&gt;}\n    &lt;\/form&gt;\n  );\n}\n<\/code><\/pre>\n<p>There&rsquo;s no clever trick here. The hook just bundles the three things every form needs (last result, action wrapper, pending bit) into one return value. I find I&rsquo;m thinking about forms less now, which is the highest compliment a hook can earn from me.<\/p>\n<p>The catch: it&rsquo;s <code>&lt;form action={...}&gt;<\/code>, not <code>onSubmit<\/code>. That trips me up about once a week when I&rsquo;m copy-pasting between files. The compiler doesn&rsquo;t warn you that an <code>onSubmit<\/code> handler is doing nothing useful next to an action prop.<\/p>\n<h2 id=\"the-react-compiler-is-the-one-i-almost-missed\">The React Compiler is the one I almost missed<\/h2>\n<p>Honestly? I almost skipped the React Compiler at first. Memoization is one of those things I assumed I was already handling correctly, and I figured the speedup wouldn&rsquo;t be worth a build-tool change.<\/p>\n<p>I was wrong about both. The compiler caught more rerenders than I expected, including a couple of context providers that were re-creating object literals on every render and triggering whole subtrees to update. I wrote about <a href=\"https:\/\/abrarqasim.com\/blog\/react-compiler-what-it-does-to-your-code\/\" rel=\"noopener\">what the React Compiler actually does to your code<\/a> once I&rsquo;d lived with it for a month, but the short version: it&rsquo;s the closest thing React has shipped to &ldquo;free performance in exchange for a config flag,&rdquo; and you should at least try it on a branch.<\/p>\n<p>Two warnings. First, it&rsquo;s strict about the rules of React. If your components have side effects in render (mine had two), the compiler will refuse to optimize them and tell you why. Fix the side effect; don&rsquo;t disable the rule. Second, it doesn&rsquo;t replace profiling. It catches the boring cases. The interesting ones, like a chart re-rendering because a parent updated <code>now<\/code>, still need eyes on the flame graph. The official <a href=\"https:\/\/react.dev\/learn\/react-compiler\" rel=\"nofollow noopener\" target=\"_blank\">React Compiler reference<\/a> covers the install path. The linting plugin is the bit I&rsquo;d install first.<\/p>\n<h2 id=\"useoptimistic-only-earns-its-keep-on-lists-not-buttons\">useOptimistic only earns its keep on lists, not buttons<\/h2>\n<p>I had high hopes for <code>useOptimistic<\/code>. In practice I use it for one thing: list updates where a server round-trip would feel laggy. Adding a comment, toggling a like, reordering items in a board.<\/p>\n<p>For single-button actions like submit, save, or delete, <code>useActionState<\/code>&rsquo;s pending flag is enough on its own. Wrapping a button in optimistic state just to flicker a label feels like reaching for a cannon to crack a walnut.<\/p>\n<p>If you want a worked example of where it actually pays off, I wrote one up in <a href=\"https:\/\/abrarqasim.com\/blog\/useoptimistic-react-19-hook-i-keep-forgetting\/\" rel=\"noopener\">the useOptimistic post I keep referring myself back to<\/a>. Short version: think of it as &ldquo;render this row immediately, then reconcile when the server replies,&rdquo; not &ldquo;add fake loading states everywhere.&rdquo;<\/p>\n<h2 id=\"server-actions-and-server-components-are-two-different-mental-models\">Server actions and server components are two different mental models<\/h2>\n<p>This one&rsquo;s caused me more confusion than anything else in React 19, and I see it in code reviews from colleagues too.<\/p>\n<p>Server components are about <em>where the JSX runs<\/em>. They render on the server, never ship JS to the client, and are great for fetch-heavy pages where the data shape is known at request time.<\/p>\n<p>Server actions are about <em>where mutations run<\/em>. They&rsquo;re functions you mark with <code>'use server'<\/code> and call from a form or a button. The transport story is RSC-flavored, but you can use server actions in a perfectly ordinary client component. I cover the day-to-day shape of this in my <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-server-actions-stopped-writing-api-routes\/\" rel=\"noopener\">post on dropping API routes for server actions<\/a>.<\/p>\n<p>The reason this trips people up: both ship under the same React 19 banner, both touch the network, and both have the word &ldquo;server&rdquo; in them. They are not the same feature. If you find yourself asking &ldquo;do I need a server component for this?&rdquo;, the answer is almost always &ldquo;no, you need a server action.&rdquo; The official <a href=\"https:\/\/react.dev\/reference\/rsc\/server-functions\" rel=\"nofollow noopener\" target=\"_blank\">server functions reference<\/a> is worth ten minutes if you&rsquo;re still fuzzy.<\/p>\n<h2 id=\"ref-as-a-prop-killed-forwardref-in-my-codebase\">ref as a prop killed forwardRef in my codebase<\/h2>\n<p>Small win, but a real one. In React 19 you can pass <code>ref<\/code> as a normal prop, and <code>forwardRef<\/code> is on its way out. My <code>Button<\/code> and <code>Combobox<\/code> components both lost a wrapper.<\/p>\n<p>Before:<\/p>\n<pre><code class=\"language-jsx\">const Button = forwardRef(function Button({ children, ...rest }, ref) {\n  return &lt;button ref={ref} {...rest}&gt;{children}&lt;\/button&gt;;\n});\n<\/code><\/pre>\n<p>After:<\/p>\n<pre><code class=\"language-jsx\">function Button({ ref, children, ...rest }) {\n  return &lt;button ref={ref} {...rest}&gt;{children}&lt;\/button&gt;;\n}\n<\/code><\/pre>\n<p>Same behavior, less ceremony. The codemod that ships with React handled most of my codebase in one pass. The one place it didn&rsquo;t was a generic <code>forwardRef&lt;HTMLDivElement, Props&gt;<\/code> that needed me to type the prop manually, but that took thirty seconds.<\/p>\n<h2 id=\"what-i-tried-and-reverted\">What I tried and reverted<\/h2>\n<p>Two features I rewrote toward and then rewrote away from.<\/p>\n<p>First, <code>use(promise)<\/code> inside client components, with Suspense boundaries everywhere. I liked the idea of data-fetching that reads as if it were synchronous. In practice it pushed me into Suspense fallback hell, where every minor loading state needed its own wrapper. For pages with one or two fetches, the older <code>useEffect + useState<\/code> (or a real data library) felt cleaner. I&rsquo;ll come back to <code>use<\/code> when I have a clearer mental model for boundary placement.<\/p>\n<p>Second, the new document metadata APIs that let you drop <code>&lt;title&gt;<\/code>, <code>&lt;meta&gt;<\/code>, and <code>&lt;link&gt;<\/code> directly inside components. They work, but my Next.js app already has <code>generateMetadata<\/code> doing the same job at the route level, and mixing the two felt fragile. If you&rsquo;re on a non-Next stack the new APIs are great. On Next, I&rsquo;d stick with what&rsquo;s already there.<\/p>\n<h2 id=\"what-to-actually-try-this-week\">What to actually try this week<\/h2>\n<p>If you&rsquo;re still on React 18, here&rsquo;s the thing I&rsquo;d do first: make a branch, install the React Compiler eslint plugin, and let it scream at you for an hour. You&rsquo;ll learn more about your codebase from those warnings than from any blog post, including this one.<\/p>\n<p>I keep notes like these in my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">work log over on abrarqasim.com<\/a> so I don&rsquo;t have to re-learn the same lesson every time React ships a feature. If a particular React 19 corner is still confusing you, ping me. I&rsquo;m always looking for the next thing to write up.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Six months after React 19 went stable, here are the features I actually use day to day, the ones I forget exist, and the ones I tried and quietly reverted.<\/p>\n","protected":false},"author":2,"featured_media":144,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Six months after React 19 went stable, here are the features I actually use day to day, the ones I forget exist, and the ones I tried and quietly reverted.","rank_math_focus_keyword":"react 19 features","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[35],"tags":[38,44,41,43,42,136,68,39],"class_list":["post-145","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-frontend","tag-javascript","tag-react","tag-react-19","tag-react-compiler","tag-useactionstate","tag-useoptimistic","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/145","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=145"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/145\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/144"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=145"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=145"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=145"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}