You are reading this on a site that uses HTMX. The navigation feels like a single-page app, there is no bundler, and every page is plain PHP that Google can read. I also build React applications for clients every month. Neither tool is "better"; they solve different shapes of problem, and picking wrong costs months. This is the comparison I wish I had read before my first over-engineered admin panel.

Two different models of where state lives

React is a client-side component model. State lives in the browser, the UI is a function of that state, and the server is a JSON API. It is extremely good at UIs where things change locally and often: a spreadsheet, a drag-and-drop board, a form with twelve dependent fields.

HTMX is a hypermedia model. The server renders HTML. Any element can issue a request (hx-get, hx-post) on any event (hx-trigger) and swap the HTML response into any target (hx-target, hx-swap). State lives on the server; the browser shows the latest rendering of it. It is extremely good at UIs where interactivity is mostly "ask the server for a new version of this bit".

REACT HTMX ┌──────────────┐ JSON ┌────────────┐ ┌──────────────┐ HTML ┌────────────┐ │ Browser │◀────────▶│ API server │ │ Browser │◀─────────▶│ Web server │ │ state + view │ │ (data) │ │ view only │ fragments │ state+view │ │ (bundle) │ └────────────┘ │ (no bundle) │ │ (PHP/Node) │ └──────────────┘ └──────────────┘ └────────────┘ rendering happens here rendering happens here

Case study: this site

Shawab.Space is a PHP site with Tailwind and a single hx-boost="true" on the <body>. That one attribute turns every internal link into an AJAX request that swaps the new page's body in and updates the URL. Navigation is instant, the browser never re-parses the <head>, and the View Transitions API animates between pages. Total JavaScript for that: 14 KB gzipped for HTMX itself.

The blog index has a real-time search and category filter. I wrote that in about 80 lines of vanilla JavaScript because the data (35 posts) is already in the DOM and filtering it client-side is trivial. If there were 3,000 posts, I would switch to an hx-get on the input with a 200 ms delay, let PHP query the database and return the filtered cards as HTML. Same UI, no framework, and the server does the work it is good at.

<!-- Server-driven search: PHP returns only the <article> cards -->
<input type="search" name="q"
       hx-get="/blog/search.php"
       hx-trigger="input changed delay:200ms, search"
       hx-target="#blog-grid"
       hx-swap="innerHTML"
       hx-indicator="#spinner"
       placeholder="Search articles">
<div id="blog-grid">…server-rendered cards…</div>

Note what is missing: no state management, no fetch wrapper, no loading-state boolean, no JSON-to-DOM rendering. HTMX handles the request, the swap, the indicator and the history. The custom WordPress themes I build use the same approach for "load more" and filtering, and clients never notice it is not React.

Where React clearly wins

I built a scheduling tool for a client last year: a weekly calendar where staff drag shifts between days, overlapping shifts highlight instantly, and the total hours recalc as you drag. Every interaction changes local state dozens of times before anything should be saved. Doing that with server round-trips would be laggy and would hammer the backend. React (with a small store) was the obvious choice and it took a few days.

The general shape of "React wins":

  • Lots of client state that changes without needing the server: editors, canvases, complex forms with cross-field logic, filtering large in-memory datasets.
  • Optimistic UI and offline behaviour: the interface must keep working while requests are in flight or failing.
  • A rich ecosystem requirement: charting, rich-text editing, virtualised lists, accessible component libraries. The React versions are years ahead.
  • A team that already thinks in components and has a design system built on them.

Modern React is also less "SPA-only" than it was. With Server Components and Server Actions, a Next.js app can render on the server and ship small client islands, which narrows the gap on the SEO and bundle-size arguments that used to push people to HTMX.

Where HTMX clearly wins

  • The server already renders HTML. WordPress, Laravel, Django, Rails, plain PHP. Adding React means a second rendering system and a JSON layer you did not need.
  • Interactivity is fragment-shaped: pagination, filters, inline edits, modals, tabs, "load more", live validation. That covers most content and admin UIs.
  • The team is backend-strong. A PHP developer is productive with HTMX in an afternoon. A React codebase needs a React developer to maintain it, forever.
  • SEO and first paint matter and budget is tight. Server HTML plus 14 KB of JS is hard to beat, and there is no hydration cost to measure against your Core Web Vitals.
  • Longevity. HTML fragments returned by a server will render in 2036. A React 18 codebase will need migrations along the way.

The decision matrix I use with clients

QuestionLeans HTMXLeans React
Does the server already render the pages?YesNo / API-first
How much state changes without a server call?LittleA lot
Do you need offline or optimistic UI?NoYes
Who maintains it in two years?Backend developersFrontend team
Is it a content site, admin panel or CRUD app?Yes-
Is it an editor, dashboard with live interactions, or app-like tool?-Yes
Do you need a component library ecosystem?RarelyOften

Score it. Three or more in one column is usually decisive. Ties go to HTMX for content and admin, React for tools.

The hybrid: HTMX pages with React islands

The projects I am proudest of use both. The page is server-rendered and HTMX-boosted; one or two complex widgets (a pricing configurator, a chart with live filters) are React components mounted into a <div id="…">. Each React island is self-contained and receives its initial data via a JSON script tag rendered by the server. When HTMX swaps a new page in, a tiny hook remounts the islands:

// islands.js  -- remount React islands after HTMX swaps
import { createRoot } from 'react-dom/client';
import PricingConfigurator from './PricingConfigurator';

const registry = { PricingConfigurator };
const roots = new WeakMap();

function mountIslands(scope = document) {
  scope.querySelectorAll('[data-island]').forEach(el => {
    if (roots.has(el)) return;
    const Component = registry[el.dataset.island];
    const props = JSON.parse(el.querySelector('script[type="application/json"]')?.textContent ?? '{}');
    const root = createRoot(el);
    root.render(<Component {...props} />);
    roots.set(el, root);
  });
}

document.addEventListener('DOMContentLoaded', () => mountIslands());
document.body.addEventListener('htmx:afterSwap', e => mountIslands(e.detail.target));
<!-- Rendered by PHP; data comes from the server, React never fetches on mount -->
<div data-island="PricingConfigurator">
  <script type="application/json"><?php echo wp_json_encode($pricing_options, JSON_HEX_TAG); ?></script>
</div>

This is the same mounting strategy I described in Integrating React into Legacy PHP, with HTMX handling the rest of the page. You get React exactly where it earns its keep and nowhere else.

Pitfalls on each side

HTMX: scripts inside swapped content run, but event listeners bound on page load do not know about new elements; use delegated listeners or re-init on htmx:afterSwap (this site's footer.php does exactly that). Design fragment endpoints as part of your API surface: validate input and check auth exactly as you would for JSON. And resist the urge to build a whole client-side state machine with hx-vals and hx-swap-oob; when you feel that urge, it is time for a React island.

React: the cost is not writing it, it is maintaining it. Budget for dependency upgrades, a build pipeline, and a developer who knows the framework. Guard your bundle size from day one. And do not use it to render a blog; the caching model is powerful but it is another thing to get right.

My honest default for a new agency project in 2026: server-rendered pages, HTMX for interactivity, React islands only where a specific widget demands it, and a full React or Next.js application only when the product is an application. It keeps sites fast, teams small and clients happy, which is the actual goal. If you are trying to decide for a specific project, I am happy to talk it through.