{"id":428,"date":"2026-07-07T05:02:48","date_gmt":"2026-07-07T05:02:48","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/htmx-vs-react-2026-when-i-actually-reach-for-htmx\/"},"modified":"2026-07-07T05:02:48","modified_gmt":"2026-07-07T05:02:48","slug":"htmx-vs-react-2026-when-i-actually-reach-for-htmx","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/htmx-vs-react-2026-when-i-actually-reach-for-htmx\/","title":{"rendered":"htmx vs React in 2026: When I Actually Reach For htmx"},"content":{"rendered":"<p>Confession: I spent about a year telling people they should try htmx and refusing to actually rewrite anything of my own in it. That&rsquo;s a very online kind of hypocrisy, and I decided to fix it earlier this year.<\/p>\n<p>I&rsquo;ve now shipped two internal tools with htmx as the main frontend, kept most of my client-heavy stuff on React 19, and I think I finally have a real opinion about when each one deserves the job.<\/p>\n<p>Short version for the impatient: if the page is mostly forms and lists, htmx is embarrassingly good. If the page is a live editor, a chat surface, a canvas, or anything with a lot of client state, keep using React. If someone tells you htmx is a &ldquo;React replacement,&rdquo; they&rsquo;ve never shipped either one.<\/p>\n<p>Here&rsquo;s the long version.<\/p>\n<h2 id=\"what-htmx-actually-is-and-what-it-isnt\">What htmx actually is (and what it isn&rsquo;t)<\/h2>\n<p>Fast recap so we&rsquo;re not arguing past each other. htmx is a small JavaScript library that lets you drive AJAX requests, WebSocket messages, and DOM swaps from HTML attributes. You write <code>hx-get<\/code>, <code>hx-post<\/code>, <code>hx-target<\/code>, and your server returns HTML fragments that htmx patches into the page. The <a href=\"https:\/\/htmx.org\/docs\/\" rel=\"nofollow noopener\" target=\"_blank\">htmx docs<\/a> call it &ldquo;high power tools for HTML.&rdquo; I keep calling it &ldquo;server-rendered HTML that finally got tired of jQuery.&rdquo;<\/p>\n<p>It is not a component framework. It doesn&rsquo;t manage client state. It doesn&rsquo;t diff a virtual DOM. It doesn&rsquo;t understand JSX.<\/p>\n<p>It is not a backend. Something on the server has to return the HTML fragments. That&rsquo;s your Laravel, Rails, Express, Go net\/http, whatever.<\/p>\n<p>It is not a replacement for React on interfaces that are genuinely apps. If your users spend an hour a day inside a canvas with dragging, panning, and undo, please stop reading and go back to React.<\/p>\n<p>If you have those expectations calibrated, everything else about htmx makes more sense.<\/p>\n<h2 id=\"the-30-second-comparison-in-code\">The 30-second comparison in code<\/h2>\n<p>Here&rsquo;s &ldquo;click a button, load the next page of posts, append it to a list.&rdquo; The classic infinite-scroll pattern.<\/p>\n<p>React (with fetch + state, no libraries beyond React 19):<\/p>\n<pre><code class=\"language-jsx\">function PostList() {\n  const [posts, setPosts] = useState([]);\n  const [page, setPage] = useState(1);\n  const [loading, setLoading] = useState(false);\n\n  async function loadMore() {\n    setLoading(true);\n    const r = await fetch(`\/api\/posts?page=${page + 1}`);\n    const j = await r.json();\n    setPosts([...posts, ...j.posts]);\n    setPage(page + 1);\n    setLoading(false);\n  }\n\n  return (\n    &lt;&gt;\n      &lt;ul&gt;{posts.map(p =&gt; &lt;li key={p.id}&gt;{p.title}&lt;\/li&gt;)}&lt;\/ul&gt;\n      &lt;button onClick={loadMore} disabled={loading}&gt;\n        {loading ? &quot;Loading...&quot; : &quot;Load more&quot;}\n      &lt;\/button&gt;\n    &lt;\/&gt;\n  );\n}\n<\/code><\/pre>\n<p>You&rsquo;ve written three pieces of state, a fetch, a JSON parse, and a re-render. It works. It&rsquo;s also the sort of code that used to embarrass me a bit until I stopped noticing.<\/p>\n<p>Same thing with htmx, where the server returns HTML fragments:<\/p>\n<pre><code class=\"language-html\">&lt;ul id=&quot;posts&quot;&gt;\n  &lt;li&gt;First post&lt;\/li&gt;\n  &lt;li&gt;Second post&lt;\/li&gt;\n&lt;\/ul&gt;\n\n&lt;button\n  hx-get=&quot;\/posts?page=2&quot;\n  hx-target=&quot;#posts&quot;\n  hx-swap=&quot;beforeend&quot;\n  hx-indicator=&quot;#spinner&quot;\n&gt;\n  Load more\n&lt;\/button&gt;\n\n&lt;span id=&quot;spinner&quot; class=&quot;htmx-indicator&quot;&gt;Loading...&lt;\/span&gt;\n<\/code><\/pre>\n<p>Your server route responds with <code>&lt;li&gt;Third post&lt;\/li&gt;&lt;li&gt;Fourth post&lt;\/li&gt;<\/code>, htmx appends them, done. There&rsquo;s no client state to worry about. The URL of the button already documents what it does.<\/p>\n<p>Now, which of those is &ldquo;better?&rdquo; It&rsquo;s the wrong question. The right question is which one is going to be easier to maintain three years and two developers from now.<\/p>\n<h2 id=\"where-htmx-quietly-wins\">Where htmx quietly wins<\/h2>\n<p>Server-rendered apps with sprinkles of interactivity are its native habitat. Admin panels, CRUD dashboards, marketing sites with a contact form. If more than 80% of your page transitions used to be full-page reloads that just replaced some markup, htmx replaces those with partial swaps and you delete a lot of code.<\/p>\n<p>Backends that already generate HTML well are the second win. Laravel Blade, Rails ERB, Django templates, Go html\/template. You&rsquo;re already good at this. You&rsquo;ve already got the layout tests. Reusing that engine and letting htmx do the swapping is a fast payoff.<\/p>\n<p>Tiny teams matter here too. htmx doesn&rsquo;t need a build pipeline. There&rsquo;s no Vite config, no bundler, no bundle-size budget. You add one script tag. If your team is one person, this compounds quickly.<\/p>\n<p>I have a small internal tool for triaging bug reports that used to be a React SPA. I rewrote it in Laravel plus htmx in a weekend and deleted about 60% of the code. It&rsquo;s not fancier. It&rsquo;s just done, and I never think about it. I write a bit more about that kind of trade-off in the notes on my <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">portfolio site<\/a>, which is itself a boring server-rendered thing that I do not miss making into an SPA.<\/p>\n<h2 id=\"where-react-still-wins\">Where React (still) wins<\/h2>\n<p>Real-time collaborative UIs are React&rsquo;s home turf. Multiple cursors, shared canvases, presence indicators. htmx can drive WebSocket messages, but any time you have complex client state that needs to reconcile with incoming events, React&rsquo;s model is a better fit.<\/p>\n<p>Forms with rich, interdependent client-side validation are another React sweet spot. The multi-step, cross-field, &ldquo;if this then that&rdquo; wizards. You can build these in htmx, but you&rsquo;ll be reinventing form state management. I&rsquo;ve written about how much cleaner these flows got with newer React primitives, especially <a href=\"https:\/\/abrarqasim.com\/blog\/react-suspense-in-production-what-i-actually-use-it-for\/\" rel=\"noopener\">React 19&rsquo;s Suspense in production<\/a> and the <a href=\"https:\/\/react.dev\/blog\/2024\/12\/05\/react-19\" rel=\"nofollow noopener\" target=\"_blank\">React 19 release notes on <code>use()<\/code> and Actions<\/a>, and the ecosystem is genuinely built for this shape of problem.<\/p>\n<p>Anything with a lot of local, ephemeral state also wants React. Text editors, live filters over big client-side lists, drag-and-drop grids. htmx wasn&rsquo;t built for these, and pretending otherwise gets you into trouble by month two.<\/p>\n<p>If your users spend most of their session inside one screen doing dozens of small interactions per minute, you probably want React. If they mostly move between pages that show them information and take input, you probably want htmx.<\/p>\n<h2 id=\"performance-and-the-less-javascript-pitch\">Performance and the &ldquo;less JavaScript&rdquo; pitch<\/h2>\n<p>The htmx marketing tends to emphasize &ldquo;ship less JavaScript.&rdquo; That&rsquo;s true. My rewritten internal tool ships about 14KB of htmx and no other JS, versus roughly 180KB of React plus client bundles before. On a fast desktop over fiber, nobody notices. On a mid-range Android over a wobbly LTE connection, it&rsquo;s the difference between a page you can use and a page you close.<\/p>\n<p>But, and this is the honest part, a well-configured React app with server components, streaming, and smart code-splitting is fine on the same network. The gap is real, it&rsquo;s just smaller than the htmx-vs-React essays tend to imply. I keep a small benchmark suite on my machine and the median LCP difference for an equivalent CRUD page is around 300ms in htmx&rsquo;s favor. That matters. It&rsquo;s not a factor-of-10 win.<\/p>\n<p>If you&rsquo;re trying to talk your team into htmx, don&rsquo;t lead with &ldquo;React is bloated.&rdquo; Lead with &ldquo;we have three developers and this page reloads five times a day.&rdquo;<\/p>\n<h2 id=\"when-to-reach-for-which-one-line-each\">When to reach for which, one line each<\/h2>\n<p>htmx: the page is mostly forms, lists, and server-rendered content, and interactivity is the exception rather than the default.<\/p>\n<p>React: the page is an application, client state is real, and the users treat it more like a tool than a document.<\/p>\n<p>Both: yes, both together. Server-render the shell with your framework and htmx, hydrate the one live widget with a React island. The <a href=\"https:\/\/htmx.org\/essays\/islands-architecture\/\" rel=\"nofollow noopener\" target=\"_blank\">htmx islands architecture essay<\/a> covers the pattern. Astro made it common, and it works surprisingly well in practice.<\/p>\n<p>Try htmx this week: pick one CRUD screen in your app. Rewrite the &ldquo;submit form, reload page&rdquo; bit to return an HTML fragment and swap it in. If you don&rsquo;t feel a bit silly about how simple it is, you did it wrong.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>htmx or React in 2026? After shipping both, here&#8217;s when I reach for htmx, when I stay on React, and why the online essays get this comparison wrong.<\/p>\n","protected":false},"author":2,"featured_media":427,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"htmx or React in 2026? After shipping both, here's when I reach for htmx, when I stay on React, and why the online essays get this comparison wrong.","rank_math_focus_keyword":"htmx vs react","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,35],"tags":[38,193,44,56,41,359,466,172],"class_list":["post-428","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-web-development","tag-frontend","tag-htmx","tag-javascript","tag-laravel","tag-react","tag-react-19-2","tag-server-rendered","tag-web-development-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/428","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=428"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/428\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/427"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=428"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=428"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=428"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}