The debate between “traditional” JavaScript frameworks like React and emerging HTML-centric approaches like HTMX has been heating up in recent months. Rather than adding to the noise with abstract comparisons, I decided to build the same interactive component using both technologies to see how they really stack up in practice.

The Toy Project: An Interactive Syntax Highlighter

For this comparison, I wanted something more interesting than the usual todo app, but still focused enough to make a fair comparison. I settled on creating an interactive code syntax highlighter with the following features:

  • A text input area where users can paste code
  • A language selector dropdown
  • Real-time syntax highlighting as you type
  • A “copy code” button with visual feedback
  • Theme switching between light and dark modes

Nothing revolutionary here, but enough to showcase architectural approaches.

The React Approach

With React, I’d naturally reach for some established libraries to handle the syntax highlighting. Prism.js or highlight.js would be good candidates, with React wrappers available for both.

Here’s what the main component structure looks like:

import React, { useState, useEffect } from 'react';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import js from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript';
import python from 'react-syntax-highlighter/dist/esm/languages/hljs/python';
import { docco, dracula } from 'react-syntax-highlighter/dist/esm/styles/hljs';

// Register languages
SyntaxHighlighter.registerLanguage('javascript', js);
SyntaxHighlighter.registerLanguage('python', python);

const CodeHighlighter = () => {
  const [code, setCode] = useState('// Type your code here');
  const [language, setLanguage] = useState('javascript');
  const [theme, setTheme] = useState('light');
  const [copied, setCopied] = useState(false);
  
  const copyToClipboard = () => {
    navigator.clipboard.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  
  return (
    <div className={`highlighter-container ${theme}`}>
      <div className="controls">
        <select 
          value={language} 
          onChange={(e) => setLanguage(e.target.value)}
        >
          <option value="javascript">JavaScript</option>
          <option value="python">Python</option>
        </select>
        
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          Toggle Theme
        </button>
        
        <button onClick={copyToClipboard}>
          {copied ? 'Copied!' : 'Copy Code'}
        </button>
      </div>
      
      <textarea
        value={code}
        onChange={(e) => setCode(e.target.value)}
        className="code-input"
      />
      
      <div className="preview">
        <SyntaxHighlighter 
          language={language} 
          style={theme === 'light' ? docco : dracula}
        >
          {code}
        </SyntaxHighlighter>
      </div>
    </div>
  );
};

export default CodeHighlighter;

This React approach gives us a clean, component-based architecture. State management is handled through React’s useState hooks, and the component re-renders whenever state changes. The syntax highlighting library does the heavy lifting for the actual code formatting.

The HTMX Approach

Now, let’s see how we might build the same thing with HTMX. The approach is fundamentally different. Instead of building a client-side application, we’ll leverage server-side rendering with targeted DOM updates.

First, our HTML structure:

<div class="highlighter-container" data-theme="light">
  <div class="controls">
    <select id="language-selector"
            hx-post="/highlight"
            hx-trigger="change"
            hx-target="#highlighted-output"
            hx-include="#code-input">
      <option value="javascript">JavaScript</option>
      <option value="python">Python</option>
    </select>
    
    <button hx-post="/toggle-theme"
            hx-target=".highlighter-container"
            hx-swap="outerHTML">
      Toggle Theme
    </button>
    
    <button hx-post="/copy"
            hx-include="#code-input"
            hx-target="this"
            hx-swap="innerHTML"
            hx-trigger="click">
      Copy Code
    </button>
  </div>
  
  <textarea id="code-input"
            hx-post="/highlight"
            hx-trigger="keyup changed delay:500ms"
            hx-target="#highlighted-output">
    // Type your code here
  </textarea>
  
  <div id="highlighted-output" class="preview">
    <!-- Initial highlighted code will be loaded here -->
  </div>
</div>

Then, on the server side (using Python with Flask as an example), we’d have:

from flask import Flask, request, render_template
import pygments
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter

app = Flask(__name__)

@app.route('/highlight', methods=['POST'])
def highlight_code():
    code = request.form.get('code-input', '// Type your code here')
    language = request.form.get('language-selector', 'javascript')
    
    try:
        lexer = get_lexer_by_name(language)
        formatter = HtmlFormatter()
        highlighted = highlight(code, lexer, formatter)
        return highlighted
    except Exception as e:
        return f"<pre>{code}</pre><div class='error'>Error: {str(e)}</div>"

@app.route('/toggle-theme', methods=['POST'])
def toggle_theme():
    current_theme = request.form.get('data-theme', 'light')
    new_theme = 'dark' if current_theme == 'light' else 'light'
    
    return render_template('highlighter_container.html', 
                           theme=new_theme,
                           code=request.form.get('code-input', ''),
                           language=request.form.get('language-selector', 'javascript'))

@app.route('/copy', methods=['POST'])
def copy_text():
    # In reality, copying happens client-side via JS
    # This endpoint just returns the confirmation message
    return "Copied!"

With a bit of extra JavaScript to handle the actual clipboard operation:

document.addEventListener('DOMContentLoaded', function() {
  htmx.on('htmx:afterSwap', function(event) {
    if (event.detail.target.id === 'highlighted-output') {
      // Apply any client-side formatting or interactions here
    }
  });
  
  // Handle actual clipboard functionality
  document.querySelector('button[hx-post="/copy"]').addEventListener('click', function() {
    const codeText = document.querySelector('#code-input').value;
    navigator.clipboard.writeText(codeText);
    
    // HTMX will handle changing button text via the response from /copy
    setTimeout(function() {
      this.innerHTML = 'Copy Code';
    }.bind(this), 2000);
  });
});

The Differences in Approach

  1. State Management: The React version manages all state in the component, while the HTMX version delegates state management to the server with targeted requests.

  2. Rendering Logic: React handles rendering on the client, while HTMX leverages server-side rendering with partially updated DOM elements.

  3. Code Organization: React centralizes UI logic in components, while HTMX distributes it between HTML attributes and server endpoints.

Performance Considerations

Running this in production reveals some interesting performance characteristics:

  • Initial Load: The HTMX version loads faster initially because it doesn’t need to download and parse a large JavaScript bundle. The React version, depending on how it’s bundled, might include 100KB+ of framework code before our application even starts.

  • Runtime Performance: For this specific example, both perform similarly once loaded. The React version feels snappier for theme switching since it’s purely client-side, while the HTMX version needs a server roundtrip.

  • Network Traffic: The HTMX version generates more HTTP requests but transfers smaller payloads (just the highlighted HTML), while the React version makes fewer requests but processes more on the client.

  • Memory Usage: The React version typically uses more browser memory due to the JavaScript runtime overhead.

Developer Experience Trade-offs

React Land

Working with both technologies on the same project revealed nuanced differences in developer experience that go beyond simple feature comparisons. In the React implementation, I found myself thinking in components from the start. The mental model feels natural once you’ve spent time with it - everything is a component with clearly defined props and state.

The debugging experience with React DevTools transformed my workflow. Being able to inspect component hierarchies, monitor state changes, and track renders made troubleshooting straightforward. When I needed to refactor my syntax highlighter to add the theme toggle feature, the component isolation made it remarkably easy to ensure I wasn’t breaking existing functionality.

HTMX World

With HTMX, the experience took a different shape. The learning curve felt gentler at first - after all, I was just writing HTML with some special attributes. This approach forced me to think more carefully about my API design upfront, considering what endpoints I needed and how they would respond to different requests.

The lack of boilerplate in HTMX was refreshing. Implementing the “copy code” button with a visual confirmation took just a few lines of markup rather than setting up state, effects, and event handlers. When building the language selector, I didn’t need to worry about managing a separate piece of state - the server simply responded with newly highlighted code when the selection changed.

One unexpected challenge with HTMX was debugging. Without a dedicated DevTools extension, I found myself relying more on browser network inspection to understand what was happening with my requests and responses.

LLM Recognition

As noted in the HTMX essay about Gumroad’s technology choices, AI tooling support is currently stronger for React. When I got stuck, LLM tools was noticeably more helpful with the React implementation than with HTMX-specific syntax and patterns.

When to Choose Which?

After building the same component both ways, I’ve developed a clearer sense of when each technology shines. React excels in scenarios where you’re crafting complex, stateful applications that require sophisticated client-side interactions. If your application needs to maintain numerous interconnected states, perform complex data manipulations in the browser, or provide rich interactive experiences without frequent server communication, React provides a robust framework to manage this complexity. This is especially true if your team already has React expertise.

HTMX, on the other hand, I found it particularly valuable when working with applications where server-side rendering already plays a significant role. The syntax highlighter demonstrated how HTMX elegantly bridges the gap between server-generated HTML and interactive user experiences. If your backend already does the heavy lifting of data processing and presentation, HTMX provides the interactivity layer without requiring a complete client-side application architecture.

The decision ultimately comes down to the specific demands of your project, your team’s expertise, and your architectural preferences. For teams tired of JavaScript framework complexity and looking for a simpler mental model, HTMX offers a refreshing alternative that still delivers modern interactivity. For applications that require complex state management and rich client-side interactions, React’s mature ecosystem continues to provide tremendous value.

Neither approach is universally “better”. They’re optimized for different contexts.

I’ve increasingly found myself reaching for HTMX for projects where I would have automatically used React a year ago. It’s about having another powerful tool in the toolbox that lets me build web applications with less complexity when appropriate.