{"id":176,"date":"2026-05-01T13:04:11","date_gmt":"2026-05-01T13:04:11","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/htmx-vs-react-2026-when-i-reach-for-each\/"},"modified":"2026-05-01T13:04:11","modified_gmt":"2026-05-01T13:04:11","slug":"htmx-vs-react-2026-when-i-reach-for-each","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/htmx-vs-react-2026-when-i-reach-for-each\/","title":{"rendered":"htmx vs React in 2026: When I Reach for Each (and When I Don&#8217;t)"},"content":{"rendered":"<p>Short version for the impatient: I rewrote our internal admin in htmx six months ago and it&rsquo;s been the best decision I made all year. I also kept our customer dashboard in React and have zero plans to change that. If you want to know why, read on.<\/p>\n<p>Both can be the right answer. Just not at the same time.<\/p>\n<h2 id=\"the-trip-that-started-this\">The trip that started this<\/h2>\n<p>In November I had a 1,200-line Next.js admin app that was three forms and a table. The forms saved data. The table showed data. There was a filter dropdown. That was the whole feature surface.<\/p>\n<p>The bundle was 84 KB gzipped. We had three useEffect bugs that month, two of them in the filter dropdown. I&rsquo;d just reread <a href=\"https:\/\/htmx.org\/essays\/locality-of-behaviour\/\" rel=\"nofollow noopener\" target=\"_blank\">Carson Gross&rsquo;s &ldquo;Locality of Behaviour&rdquo; essay<\/a> and decided to try htmx for one tiny side feature. Two weekends later I&rsquo;d ported the whole panel. The new admin is 11 KB of HTML, no build step, no useEffect, no zustand, no react-query. It runs.<\/p>\n<p>Then I tried to do the same thing with our customer-facing dashboard. I lasted about six hours before I came crawling back to React. So here we are.<\/p>\n<h2 id=\"what-htmx-actually-is-and-isnt\">What htmx actually is (and isn&rsquo;t)<\/h2>\n<p>htmx is roughly 14 KB of JavaScript that adds attributes like <code>hx-get<\/code>, <code>hx-post<\/code>, and <code>hx-target<\/code> to plain HTML. Your server returns HTML fragments instead of JSON. The library swaps those fragments into the page.<\/p>\n<p>That&rsquo;s the whole library. There&rsquo;s no virtual DOM, no component tree, no hydration. You&rsquo;re rendering on the server, your app&rsquo;s &ldquo;state&rdquo; lives in the database, and the browser is mostly responsible for showing what the server sends.<\/p>\n<p>A typical interaction looks like this:<\/p>\n<pre><code class=\"language-html\">&lt;button hx-post=&quot;\/api\/items&quot;\n        hx-target=&quot;#items-list&quot;\n        hx-swap=&quot;afterbegin&quot;&gt;\n  Add item\n&lt;\/button&gt;\n&lt;ul id=&quot;items-list&quot;&gt;\n  &lt;!-- server returns a new &lt;li&gt; and htmx prepends it here --&gt;\n&lt;\/ul&gt;\n<\/code><\/pre>\n<p>Compare that to the React equivalent:<\/p>\n<pre><code class=\"language-jsx\">function AddItem() {\n  const queryClient = useQueryClient()\n  const mutation = useMutation({\n    mutationFn: () =&gt;\n      fetch('\/api\/items', { method: 'POST' }).then(r =&gt; r.json()),\n    onSuccess: (item) =&gt; {\n      queryClient.setQueryData(['items'], old =&gt; [item, ...old])\n    },\n  })\n\n  return (\n    &lt;button onClick={() =&gt; mutation.mutate()} disabled={mutation.isPending}&gt;\n      {mutation.isPending ? 'Adding...' : 'Add item'}\n    &lt;\/button&gt;\n  )\n}\n<\/code><\/pre>\n<p>The React version is more flexible. It also has roughly four extra abstractions to learn before it works. For a simple admin form, that&rsquo;s a tax I keep paying with no benefit.<\/p>\n<p>That said, htmx is not a SPA framework with extra steps. It&rsquo;s a different shape of app. If you want offline support, optimistic UI that survives network failures, or interactive client state that doesn&rsquo;t round-trip, you&rsquo;re going to fight htmx and lose.<\/p>\n<h2 id=\"where-htmx-beat-react-for-me\">Where htmx beat React for me<\/h2>\n<p>Three places, specifically.<\/p>\n<p>First, internal CRUD. Admin panels, team dashboards, anything where the user is technical and on a connection we control. The &ldquo;render full HTML on the server&rdquo; model is ideal here. Round-trips are cheap, and I get to use whatever templating language my backend already speaks. I&rsquo;m running PHP for the admin, so I get <a href=\"https:\/\/abrarqasim.com\/blog\/php-84-property-hooks-changed-how-i-write-classes\" rel=\"noopener\">property hooks and asymmetric visibility<\/a> in my view models for free. No serialization layer between the model and the screen.<\/p>\n<p>Second, forms with server validation. Every form has the same lifecycle: type, submit, validate, return errors or success. In React I usually write validation rules twice, once on the client with Zod, once on the server in whatever language the backend speaks. With htmx I write it once on the server and return the form HTML with the errors already in place. <code>hx-post=\"\/items\"<\/code> on the form, <code>hx-target=\"this\"<\/code> and <code>hx-swap=\"outerHTML\"<\/code>, and the form replaces itself with whatever the server sent. That&rsquo;s the whole feature.<\/p>\n<p>Third, pages that mostly aren&rsquo;t interactive. Most pages on most apps are text and a couple of buttons. React&rsquo;s fixed cost (download, parse, hydrate) is the same whether the page does 100 things or 1. htmx&rsquo;s fixed cost scales with what&rsquo;s actually on the page.<\/p>\n<p>The killer thing for me is the boring debugging story. When something breaks, the answer is in the network tab. A request and a response, both HTML. I can read both. No virtual DOM diff, no React DevTools, no question of which render this is.<\/p>\n<h2 id=\"where-i-went-back-to-react\">Where I went back to React<\/h2>\n<p>I tried to port the customer dashboard and bailed. Here are the reasons, in order of how much they hurt.<\/p>\n<p>Real-time updates first. The dashboard streams pricing updates over a WebSocket. I had this working in React with a thin reducer. In htmx I ended up with a server-sent-events extension, a hidden div, and a workflow that swapped pricing rows individually as updates came in. It worked for a single tab. With three tabs open and updates rolling in, the layout would flicker. I&rsquo;m sure someone smarter than me has solved this. I just didn&rsquo;t want to spend a week on it.<\/p>\n<p>The filter UI was second. Multi-select with chips, a date range, three numeric ranges, all combinable. Every filter change should update the visible result without a page reload. In htmx that&rsquo;s seven hidden inputs and a hairy <code>hx-include<\/code> attribute. In React it&rsquo;s a <code>useReducer<\/code> with a <code>filters<\/code> object. The React version is shorter and easier to test.<\/p>\n<p>Drag-and-drop reordering came third. htmx has a <a href=\"https:\/\/htmx.org\/extensions\/sortable\/\" rel=\"nofollow noopener\" target=\"_blank\">Sortable extension<\/a> and it works for simple lists. We had nested groups with constraints, where you could drag a card into one group but not another. I built a small proof of concept and decided I&rsquo;d rather use <a href=\"https:\/\/dndkit.com\" rel=\"nofollow noopener\" target=\"_blank\">dnd-kit<\/a> and call it a day.<\/p>\n<p>Animations that depend on outgoing state were last. <code>hx-swap<\/code> has a <code>transition:true<\/code> option that uses the View Transitions API, and it handled most of what I needed. For a drawer that slides out while a new view slides in with the old DOM still measurable, I gave up.<\/p>\n<p>The pattern: anywhere the user expects rich interactive feedback that doesn&rsquo;t map cleanly to &ldquo;submit something, get HTML back,&rdquo; React earns its weight.<\/p>\n<h2 id=\"how-i-actually-decide-now\">How I actually decide now<\/h2>\n<p>I ask myself three questions.<\/p>\n<p>Is the user&rsquo;s network reliable? If they&rsquo;re in our office or on a paid product where we control the experience, htmx is fine. If they&rsquo;re mobile users on patchy connections who expect &ldquo;it always feels fast,&rdquo; React with proper loading states wins.<\/p>\n<p>How much state lives only in the browser? A dashboard with filters, a sort order, and a currently selected row can be modeled as URL params and server state. That&rsquo;s htmx territory. A real-time multi-user collaboration tool with cursors and presence is React territory. Or, honestly, <a href=\"https:\/\/liveblocks.io\/\" rel=\"nofollow noopener\" target=\"_blank\">a dedicated tool like Liveblocks<\/a>.<\/p>\n<p>Who&rsquo;s writing this? htmx assumes you can write good HTML and that your server is fast. If your team is mostly frontend specialists who don&rsquo;t want to think about templates, React is a kinder default. I&rsquo;m a full-stack person and I like the htmx model. Not everyone does.<\/p>\n<p>The thing I want to push back on: this isn&rsquo;t a new vs. old debate. It isn&rsquo;t htmx-the-startup-killer against React-the-incumbent. They&rsquo;re solving overlapping but different problems, and the right move is usually to pick per-feature, not per-app. I&rsquo;ve shipped a Next.js app that calls <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-server-actions-stopped-writing-api-routes\" rel=\"noopener\">server actions on a few routes<\/a> and uses full client components on others. It&rsquo;s fine. Users don&rsquo;t care.<\/p>\n<h2 id=\"what-id-try-this-week-if-i-were-starting-over\">What I&rsquo;d try this week if I were starting over<\/h2>\n<p>If you&rsquo;ve never touched htmx, build one thing with it. Pick the most boring CRUD form in your app, the one with two inputs and a save button. Try replacing it with htmx in an afternoon. Two outcomes are possible.<\/p>\n<p>It&rsquo;s faster than your React version, in development time and at runtime, and you start asking what else you can simplify. That&rsquo;s what happened to me.<\/p>\n<p>Or it feels constraining and you go back to React with a clearer sense of what React was actually doing for you. That&rsquo;s also a useful outcome.<\/p>\n<p>If you want a starting point, the <a href=\"https:\/\/htmx.org\/examples\/\" rel=\"nofollow noopener\" target=\"_blank\">htmx examples gallery<\/a> is the best learning resource I&rsquo;ve seen for any web library in years. Each example is a single HTML file you can read in sixty seconds. It&rsquo;s the opposite of the React docs, in a good way.<\/p>\n<p>I write up more of these tool-vs-tool comparisons over on <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my work page<\/a> if that kind of thing helps.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I rewrote our admin panel in htmx and kept our dashboard in React. Here&#8217;s how the two compare in 2026, with code, gotchas, and when each one wins.<\/p>\n","protected":false},"author":2,"featured_media":175,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I rewrote our admin panel in htmx and kept our dashboard in React. Here's how the two compare in 2026, with code, gotchas, and when each one wins.","rank_math_focus_keyword":"htmx vs react","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,35],"tags":[38,193,44,41,39],"class_list":["post-176","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-web-development","tag-frontend","tag-htmx","tag-javascript","tag-react","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/176","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=176"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/176\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/175"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=176"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=176"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=176"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}