{"id":308,"date":"2026-06-03T05:05:03","date_gmt":"2026-06-03T05:05:03","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/htmx-for-react-developers-mental-model-i-wish-id-had\/"},"modified":"2026-06-03T05:05:03","modified_gmt":"2026-06-03T05:05:03","slug":"htmx-for-react-developers-mental-model-i-wish-id-had","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/htmx-for-react-developers-mental-model-i-wish-id-had\/","title":{"rendered":"Htmx for React Developers: The Mental Model I Wish I&#8217;d Had"},"content":{"rendered":"<p>Confession: I tried htmx three times before any of it stuck. The first two times I read the docs, built a task list, decided I missed JSX, and went back to React. The third time someone explained the mental model in a single sentence and the whole thing finally clicked. The sentence: &ldquo;In htmx, the server returns HTML fragments and the page swaps them in.&rdquo; That&rsquo;s it. That&rsquo;s the whole framework. Everything else is plumbing.<\/p>\n<p>If you&rsquo;re a React developer reading the <a href=\"https:\/\/htmx.org\/docs\/\" rel=\"nofollow noopener\" target=\"_blank\">official htmx docs<\/a> and finding yourself confused, this post is for you. I&rsquo;m not going to convert you. I&rsquo;m going to give you the bridge between &ldquo;build a virtual DOM and reconcile&rdquo; and &ldquo;return HTML strings from your server.&rdquo;<\/p>\n<h2 id=\"the-mental-model-in-one-paragraph\">The mental model in one paragraph<\/h2>\n<p>In React, your component tree lives in JavaScript memory. State changes trigger a render. The render produces a new virtual DOM. The reconciler diffs it against the previous one and updates the real DOM.<\/p>\n<p>In <a href=\"https:\/\/htmx.org\/\" rel=\"nofollow noopener\" target=\"_blank\">htmx<\/a>, there is no component tree in the browser. There is a real DOM, period. When the user clicks a button or submits a form, htmx makes an HTTP request to your server. Your server returns an HTML fragment. Htmx swaps that fragment into a specific spot in the DOM. The page updates because the DOM was literally replaced, not because a reconciler decided to update it.<\/p>\n<p>If that sounds primitive, it is. It is also exactly how the web worked for fifteen years before React existed, with the one important change that htmx lets you swap <em>fragments<\/em> instead of full pages.<\/p>\n<h2 id=\"the-four-patterns-i-actually-ship\">The four patterns I actually ship<\/h2>\n<h3 id=\"1-inline-edit-with-hx-get-and-hx-post\">1. Inline edit with <code>hx-get<\/code> and <code>hx-post<\/code><\/h3>\n<p>This is the canonical example because it&rsquo;s the one that lets you stop writing React state for forms:<\/p>\n<pre><code class=\"language-html\">&lt;!-- Display mode --&gt;\n&lt;div id=&quot;user-row-42&quot;&gt;\n  &lt;span&gt;Name: Jane Doe&lt;\/span&gt;\n  &lt;button hx-get=&quot;\/users\/42\/edit&quot;\n          hx-target=&quot;#user-row-42&quot;\n          hx-swap=&quot;outerHTML&quot;&gt;\n    Edit\n  &lt;\/button&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>When the button is clicked, htmx GETs <code>\/users\/42\/edit<\/code>. The server returns the edit form as an HTML fragment:<\/p>\n<pre><code class=\"language-html\">&lt;!-- Edit mode (returned by \/users\/42\/edit) --&gt;\n&lt;form id=&quot;user-row-42&quot;\n      hx-post=&quot;\/users\/42&quot;\n      hx-target=&quot;this&quot;\n      hx-swap=&quot;outerHTML&quot;&gt;\n  &lt;input name=&quot;name&quot; value=&quot;Jane Doe&quot;&gt;\n  &lt;button type=&quot;submit&quot;&gt;Save&lt;\/button&gt;\n&lt;\/form&gt;\n<\/code><\/pre>\n<p>The form replaces the original div. On save, the server returns the display fragment again. No client-side state. No <code>useState<\/code>, no <code>useReducer<\/code>, no React Query cache to invalidate. Two endpoints and two HTML templates.<\/p>\n<p>The React equivalent is roughly fifty lines once you include the optimistic update, the loading state, the error boundary, and the test for the loading state. The htmx version is fifteen lines of HTML across two templates.<\/p>\n<h3 id=\"2-lazy-loaded-sections-with-hx-triggerintersect\">2. Lazy-loaded sections with <code>hx-trigger=\"intersect\"<\/code><\/h3>\n<p>For a slow-to-render section that I don&rsquo;t want blocking the initial page:<\/p>\n<pre><code class=\"language-html\">&lt;div hx-get=&quot;\/dashboard\/revenue&quot;\n     hx-trigger=&quot;intersect once&quot;\n     hx-swap=&quot;outerHTML&quot;&gt;\n  &lt;div class=&quot;skeleton&quot;&gt;Loading revenue...&lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>The section loads the moment it scrolls into view. The server returns the rendered HTML. No <code>useEffect<\/code>, no <code>IntersectionObserver<\/code> polyfill, no loading-state state machine.<\/p>\n<h3 id=\"3-polling-with-hx-triggerevery-5s\">3. Polling with <code>hx-trigger=\"every 5s\"<\/code><\/h3>\n<p>Real-time dashboards that don&rsquo;t actually need WebSockets:<\/p>\n<pre><code class=\"language-html\">&lt;div hx-get=&quot;\/jobs\/123\/status&quot;\n     hx-trigger=&quot;every 5s&quot;\n     hx-swap=&quot;innerHTML&quot;&gt;\n  pending\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>When the server is satisfied the job is done, it returns the final status without the <code>hx-trigger<\/code> attribute, and polling stops. This is the pattern I reach for whenever someone says &ldquo;we need real-time&rdquo; and they actually mean &ldquo;five-second freshness is fine.&rdquo;<\/p>\n<h3 id=\"4-out-of-band-swaps-for-update-multiple-regions\">4. Out-of-band swaps for &ldquo;update multiple regions&rdquo;<\/h3>\n<p>This is the pattern that took me longest to internalize. After a form submit, you often want to update two unrelated parts of the page (say, a sidebar count and the main table). React would do this with shared state. Htmx does it with <code>hx-swap-oob<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;!-- Server response to POST \/items --&gt;\n&lt;div id=&quot;item-list&quot;&gt;\n  &lt;!-- updated list --&gt;\n&lt;\/div&gt;\n&lt;span id=&quot;item-count&quot; hx-swap-oob=&quot;true&quot;&gt;7&lt;\/span&gt;\n<\/code><\/pre>\n<p>The primary swap target updates as normal. Any element with <code>hx-swap-oob=\"true\"<\/code> in the response is also swapped into wherever its <code>id<\/code> matches on the page. Two regions, one request, no shared state.<\/p>\n<h2 id=\"what-htmx-is-bad-at\">What htmx is bad at<\/h2>\n<p>Not every UI fits this model. The cases where I still reach for React:<\/p>\n<ul>\n<li><strong>Highly interactive editors.<\/strong> Anything where the UI state has its own logic that needs to be responsive to keystrokes (rich text editors, design tools, anything draggable with complex hit-testing). The roundtrip cost dominates.<\/li>\n<li><strong>Offline-first apps.<\/strong> Htmx assumes a network. If your app needs to work without one, you need real client-side state and probably a service worker, not htmx.<\/li>\n<li><strong>Apps with deep client-side state shared across many components.<\/strong> A spreadsheet&rsquo;s selection model, for example, doesn&rsquo;t want to be a server round trip per cell click.<\/li>\n<\/ul>\n<p>The rule I use: if the user&rsquo;s next action depends on a UI state the server cannot reasonably know, htmx fights you. Otherwise it gets out of the way.<\/p>\n<h2 id=\"the-part-i-underrated-server-side-templating-gets-fun-again\">The part I underrated: server-side templating gets fun again<\/h2>\n<p>When the unit of UI is an HTML fragment your backend returns, your server templates are doing real work. I&rsquo;ve been writing more <a href=\"https:\/\/abrarqasim.com\/blog\/filament-php-the-admin-panel-i-stopped-rolling-myself\" rel=\"noopener\">Filament-style admin code with PHP<\/a> because the loop got short again. Two minutes per change instead of ten because there&rsquo;s no bundler to wait on.<\/p>\n<p>I write my htmx backends in three languages depending on the project: Go with <code>html\/template<\/code>, Laravel with Blade, and Hono with JSX. All three are fine. The framework matters less than the discipline of returning fragments instead of full pages.<\/p>\n<h2 id=\"when-i-reach-for-htmx-vs-react\">When I reach for htmx vs React<\/h2>\n<p>If I had to compress six months of opinion into a heuristic:<\/p>\n<p>Use htmx for CRUD apps, dashboards that don&rsquo;t need keystroke-level interactivity, internal tools, and anything where the user&rsquo;s mental model is &ldquo;submit a thing, see the new state.&rdquo; Use React for editors, charts that respond to mouse moves, anything offline-capable, or anything where the design has been driven by a designer who assumes a JavaScript framework.<\/p>\n<p>This matches roughly what I said in <a href=\"https:\/\/abrarqasim.com\/blog\/htmx-vs-react-2026-when-i-reach-for-each\" rel=\"noopener\">the htmx vs React decision post I wrote earlier<\/a>, but the angle there was framework-vs-framework. The angle here is &ldquo;how do I think about it once I&rsquo;ve picked it.&rdquo;<\/p>\n<h2 id=\"one-concrete-thing-to-do-this-week\">One concrete thing to do this week<\/h2>\n<p>Pick the smallest form in your app. The one nobody loves writing. The user-profile-edit form, the password-change form, the boring one. Replace its React implementation with an htmx version that GETs an edit fragment, POSTs to save, and returns a display fragment. Time how long it takes you to ship versus how long the React version took. Decide what to do with that signal.<\/p>\n<p>My own <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">recent client work<\/a> has included a couple of internal-tool rewrites where the React-to-htmx swap halved the codebase and the team&rsquo;s bug count went down. Your mileage may vary. The whole point of the exercise is to find out.<\/p>\n<p>Htmx is not a React replacement. It&rsquo;s a different tool for the same job, and the job it does is narrower and older than people give it credit for.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Most htmx tutorials skip the part that confuses React developers. Here&#8217;s the mental model I needed before any of it clicked, plus the four patterns I actually ship.<\/p>\n","protected":false},"author":2,"featured_media":307,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Most htmx tutorials skip the part that confuses React developers. Here's the mental model I needed before any of it clicked, plus the four patterns I actually ship.","rank_math_focus_keyword":"htmx tutorial","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138],"tags":[38,350,193,41],"class_list":["post-308","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","tag-frontend","tag-html","tag-htmx","tag-react"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/308","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=308"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/308\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/307"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=308"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=308"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=308"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}