{"id":597,"date":"2026-08-20T13:04:14","date_gmt":"2026-08-20T13:04:14","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/htmx-2026-where-i-use-it-and-where-i-still-reach-for-react\/"},"modified":"2026-08-20T13:04:14","modified_gmt":"2026-08-20T13:04:14","slug":"htmx-2026-where-i-use-it-and-where-i-still-reach-for-react","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/htmx-2026-where-i-use-it-and-where-i-still-reach-for-react\/","title":{"rendered":"htmx in 2026: Where I Use It, and Where I Still Reach for React"},"content":{"rendered":"<p>I spent most of 2024 being quietly rude about htmx. Not loudly, I never wrote the post, but in my head it was filed under &ldquo;things Twitter likes that don&rsquo;t survive a real project.&rdquo; Then last year I built an admin panel for a client, badly, in React, and spent three days debugging a stale cache in a table that displayed forty rows of data and had exactly one filter.<\/p>\n<p>Three days. Forty rows. I rebuilt that screen with htmx over a weekend and it has needed no maintenance since.<\/p>\n<p>So I was wrong, but not in the way htmx advocates usually want me to be wrong. Here&rsquo;s where I actually land now, including the parts where I still reach for React and don&rsquo;t apologise for it.<\/p>\n<h2 id=\"what-htmx-is-minus-the-manifesto\">What htmx is, minus the manifesto<\/h2>\n<p>htmx lets you put HTTP requests on HTML attributes and swap the response into the DOM. That&rsquo;s the whole library. Your server returns HTML fragments instead of JSON, and there is no client-side model to keep in sync, because there is no client-side model.<\/p>\n<p>The React version of a &ldquo;load comments when clicked&rdquo; button:<\/p>\n<pre><code class=\"language-jsx\">function Comments({ postId }) {\n  const [comments, setComments] = useState(null);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n\n  async function load() {\n    setLoading(true);\n    try {\n      const res = await fetch(`\/api\/posts\/${postId}\/comments`);\n      if (!res.ok) throw new Error(res.statusText);\n      setComments(await res.json());\n    } catch (e) {\n      setError(e);\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  if (error) return &lt;p&gt;Something broke.&lt;\/p&gt;;\n  if (loading) return &lt;Spinner \/&gt;;\n  if (!comments) return &lt;button onClick={load}&gt;Load comments&lt;\/button&gt;;\n  return &lt;ul&gt;{comments.map(c =&gt; &lt;li key={c.id}&gt;{c.body}&lt;\/li&gt;)}&lt;\/ul&gt;;\n}\n<\/code><\/pre>\n<p>The htmx version:<\/p>\n<pre><code class=\"language-html\">&lt;button hx-get=&quot;\/posts\/42\/comments&quot;\n        hx-target=&quot;#comments&quot;\n        hx-swap=&quot;innerHTML&quot;&gt;\n  Load comments\n&lt;\/button&gt;\n&lt;div id=&quot;comments&quot;&gt;&lt;\/div&gt;\n<\/code><\/pre>\n<p>And on the server, you render the same partial template you&rsquo;d render anywhere else. No serialiser, no API contract, no second representation of a comment that has to stay in step with the first one.<\/p>\n<p>That last point is the actual argument, and it took me too long to hear it. The cost of a JSON API isn&rsquo;t the endpoint. It&rsquo;s that you now maintain two models of every object and a translation layer between them, forever.<\/p>\n<h2 id=\"the-rule-i-use-to-decide\">The rule I use to decide<\/h2>\n<p>Carson Gross, who wrote htmx, has an essay called <a href=\"https:\/\/htmx.org\/essays\/when-to-use-hypermedia\/\" rel=\"nofollow noopener\" target=\"_blank\">When Should You Use Hypermedia?<\/a> and his framing is that apps like Google Sheets and Google Maps aren&rsquo;t candidates, because they have large amounts of interdependent state and need to respond to mouse movement faster than a server round trip allows. Almost everything else is fair game.<\/p>\n<p>My version is blunter. I ask one question: does this screen need to stay correct while the user is offline or mid-gesture?<\/p>\n<p>Drag-and-drop reordering, a canvas, a rich text editor, a spreadsheet grid, an optimistic UI that must feel instant on a slow connection. Those need client state, and I&rsquo;ll use React or Vue, and I won&rsquo;t feel bad about it.<\/p>\n<p>A CRUD table, a form, a filter, a dashboard, an admin panel, a settings page, a checkout. Those need a server round trip anyway. Wrapping them in a framework that maintains a virtual DOM so it can re-render data that came from the server two seconds ago is work you&rsquo;re doing for no one.<\/p>\n<p>Roughly eighty percent of the screens I&rsquo;ve been paid to build fall in the second group. That number surprised me when I counted.<\/p>\n<h2 id=\"htmx-4-is-coming-and-the-reason-is-interesting\">htmx 4 is coming, and the reason is interesting<\/h2>\n<p>Gross said publicly there would never be an htmx 3. He then announced <a href=\"https:\/\/htmx.org\/essays\/the-fetchening\/\" rel=\"nofollow noopener\" target=\"_blank\">htmx 4<\/a>, on the grounds that he&rsquo;d never said anything about a version four. I laughed harder at that than I should have.<\/p>\n<p>The joke is doing real work, though. The big change is replacing <code>XMLHttpRequest<\/code> with <code>fetch()<\/code>, which lets streaming responses and server-sent events move into core rather than living as extensions, and lets DOM morphing come along too.<\/p>\n<p>The other change is the one I care about more. In htmx 2, attribute inheritance is implicit. A <code>hx-target<\/code> on a parent quietly applies to every child. Gross calls this the biggest mistake in htmx 1 and 2, says he was inspired by CSS, and says the results were &ldquo;powerful and maddening&rdquo;, which is the most accurate description of CSS inheritance I&rsquo;ve read.<\/p>\n<p>In htmx 4, inheritance is explicit:<\/p>\n<pre><code class=\"language-html\">&lt;div hx-target:inherited=&quot;#output&quot;&gt;\n  &lt;button hx-post=&quot;\/up&quot;&gt;Like&lt;\/button&gt;\n  &lt;button hx-post=&quot;\/down&quot;&gt;Dislike&lt;\/button&gt;\n&lt;\/div&gt;\n&lt;output id=&quot;output&quot;&gt;Pick a button...&lt;\/output&gt;\n<\/code><\/pre>\n<p>Without that <code>:inherited<\/code> modifier, the buttons don&rsquo;t pick up the target. You can turn old behaviour back on with a config flag if you need to.<\/p>\n<p>History handling changes too: htmx 4 stops snapshotting the DOM into local storage and just re-requests the content instead. Slower on paper. Correct in practice, because DOM snapshots break the moment a third-party script touches the page, and the standard advice for history bugs in htmx 2 was already &ldquo;turn the cache off.&rdquo;<\/p>\n<p>If you&rsquo;re on htmx 2 and nervous: the <a href=\"https:\/\/htmx.org\/essays\/the-fetchening\/\" rel=\"nofollow noopener\" target=\"_blank\">project page says 2.0 will be supported in perpetuity<\/a> and 4.0 rolls out over multiple years, with 4.x sitting on <code>next<\/code> for a long time before it becomes <code>latest<\/code>. There&rsquo;s no cliff here.<\/p>\n<h2 id=\"the-caching-thing-nobody-mentions\">The caching thing nobody mentions<\/h2>\n<p>Here&rsquo;s a practical difference that took me a while to notice and that I now bring up early.<\/p>\n<p>When your server returns JSON, your HTML shell is static and cacheable at the edge, and the data arrives separately. When your server returns HTML fragments, the fragment is the response, and its cacheability depends entirely on whether that fragment is personalised. A comments list is probably cacheable. A comments list with an &ldquo;edit&rdquo; button that only appears for the author is not, at least not without varying on something.<\/p>\n<p>This isn&rsquo;t an argument against htmx. It&rsquo;s an argument for deciding, per fragment, whether it&rsquo;s public or personal, and being disciplined about not mixing the two in one response. In practice I split them: the public list is one endpoint, the per-user controls are a small out-of-band swap. It&rsquo;s slightly more endpoints and dramatically simpler cache headers.<\/p>\n<p>The related point is that your server rendering has to be quick, because now it&rsquo;s on the critical path of every interaction rather than just the initial page load. A template render that takes 80ms was invisible when it happened once. At 80ms per filter change, users notice. If your framework has a template cache, turn it on before you benchmark anything and blame htmx for a number your renderer produced.<\/p>\n<h2 id=\"where-htmx-bit-me\">Where htmx bit me<\/h2>\n<p>It isn&rsquo;t free. Three things I hit on real work.<\/p>\n<p>Your templates fragment. You end up with a lot of small partials, and if your template language has no good story for composing them, you get a directory of forty files named things like <code>_row_edit_mode.html<\/code>. That&rsquo;s manageable. It&rsquo;s also not the tidy story the demos tell.<\/p>\n<p>Form validation gets chatty. Every keystroke-level validation is a request. You can debounce with <code>hx-trigger=\"keyup changed delay:500ms\"<\/code>, and you should, but it&rsquo;s a decision you now have to make on every field rather than something the client framework handled for free.<\/p>\n<p>And you will still write JavaScript. Not much, and the htmx docs point you at Alpine for it, but the &ldquo;zero JavaScript&rdquo; framing is marketing. Budget for a little.<\/p>\n<p>The one I underestimated most, though, was testing. With a JSON API you can test the endpoint&rsquo;s contract independently of how anything renders it, and that separation is genuinely useful when the frontend and backend change at different speeds. With HTML fragments, your endpoint test is asserting on markup, which is more brittle and couples your test suite to your templates. There are reasonable answers, mostly asserting on a handful of stable selectors rather than whole strings, but &ldquo;reasonable answers&rdquo; is doing real work in that sentence. If you have a mobile client consuming the same API, none of this applies, because you needed the JSON anyway and htmx was never the right call.<\/p>\n<p>There&rsquo;s also a team dimension I&rsquo;ve stopped pretending doesn&rsquo;t matter. If your frontend and backend people are different humans with different repos, HTML fragments put the rendering decision on the backend side of that line, and some teams are organised in a way that makes this genuinely painful. The technology is fine. The org chart might not be.<\/p>\n<h2 id=\"how-this-sits-next-to-server-actions\">How this sits next to server actions<\/h2>\n<p>If you&rsquo;re in the Next.js world, none of this is unfamiliar. Server actions are chasing the same idea from the other direction: keep the mutation on the server, stop maintaining an API layer whose only client is your own frontend. I wrote about <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-server-actions-two-rules-i-learned-the-hard-way\/\" rel=\"noopener\">the two rules I learned the hard way with server actions<\/a>, and the underlying instinct is identical.<\/p>\n<p>The difference is what you ship to the browser. Server actions still hand the client a React runtime. htmx hands it about 14kb and some attributes. Which trade is right depends on whether the rest of your app needs the runtime anyway. If it does, use the thing you already have. If your app is mostly forms and tables and you&rsquo;re pulling in React solely to render them, that&rsquo;s the case where I&rsquo;d genuinely stop and reconsider.<\/p>\n<h2 id=\"try-this-one-thing\">Try this one thing<\/h2>\n<p>Pick the dullest screen in your current project. The settings page, the one nobody demos. Rebuild it with htmx from a CDN script tag and your existing server templates, in one sitting, without deleting anything.<\/p>\n<p>If it takes longer than an afternoon, your server-side rendering story is the problem, not htmx, and that&rsquo;s useful information too. If it takes an hour, you&rsquo;ve learned something about how much framework your dull screens have been carrying. Most of the <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">client work I take on<\/a> has at least three screens like this, and they&rsquo;re usually the ones generating support tickets.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Where htmx earns its place, where I still reach for React, what htmx 4 changes with fetch and explicit inheritance, and the costs the demos leave out.<\/p>\n","protected":false},"author":2,"featured_media":596,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Where htmx earns its place, where I still reach for React, what htmx 4 changes with fetch and explicit inheritance, and the costs the demos leave out.","rank_math_focus_keyword":"htmx","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,35],"tags":[38,193,650,41,39],"class_list":["post-597","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-web-development","tag-frontend","tag-htmx","tag-hypermedia","tag-react","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/597","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=597"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/597\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/596"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=597"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=597"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=597"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}