DEV Community

Cover image for Why I Chose HTMX Over React for My SaaS (And What Happened)
Rocky Lott
Rocky Lott

Posted on

Why I Chose HTMX Over React for My SaaS (And What Happened)

The Setup

I decided I was going to write a book. Wait. No. 5 books in a southern gothic meets urban fantasy series! How cool would that be? Even if no one else read it? So I came up with a character, then wished I had a character sheet like they have in D&D. I searched for writing tools and decided to use Scrivener. Now that was a steep learning curve, and it didn't really do what I wanted. It had ways to organize things, but the search was clunky and there wasn't really any way to "see" the big picture the way I wanted to. Where were the characters last? Were there any plot holes? Does this draft suck? I thought AutoCrit was going to be the answer to this problem, and it was "smarter" than Scrivener in a lot of ways, and not as smart in others. One of the biggest challenges with both platforms was cross-device access. So what does a developer-minded guy like me do when he wants to build a world, track all of his changes, and be able to work from any internet connected device?

The Failed Attempts

An Electron app couldn't be that hard to develop, right? Down the rabbit hole I went, creating text parsers and database mechanics to track all of the world building that I wanted to do. HTML, CSS, totally in my wheelhouse. JavaScript, not as much, but it got better as I went. I decided it would be cool if I could have someone review my code, just to make sure I was on the right track. ChatGPT could do that, right? It felt like I had a coding partner, and for weeks we worked on the app while I kept writing the book in the background, using the latest version to test our analysis and import tools.

Here's where it went sideways. Electron has two worlds — the main process and the renderer — and they talk to each other through IPC handlers or the preload bridge. ChatGPT could never pick one. We'd build a tool, wire it up through IPC in main.js, get it working. Next session, ChatGPT would rip that wiring out and redo it through the preload bridge instead. Then we'd add the next feature, and it would go back to IPC. Months of this. The codebase turned into a frankenstein of both patterns fighting each other, and half the time a "fix" for one tool would break three others because nothing was consistent.

I tried bolting on React at one point too — there's still a vite-react-ui folder in that graveyard to prove it. That lasted about a week before I realized I was now managing Electron IPC, a preload bridge, React state, AND a SQLite database all talking past each other. For a writing app. Built by one guy. After work.

That's when I took a step back. The app worked, sort of, but it was fragile. It only ran on one machine. Syncing the database between my computers meant trusting Dropbox with a SQLite file, which is exactly as reliable as it sounds. I wanted to write from anywhere. I gave up on the Electron app, and honestly, I gave up on ChatGPT writing code too. Figured I'd just use Scrivener and AutoCrit like everyone else until I had more time and money to throw at the problem. I went back to just writing.

Then I got Claude Code. Different experience entirely — not a chatbot I was copying and pasting from, but something that could actually work in my codebase, see my files, understand what was already there. We tried again as a web app. Flask backend, JavaScript SPA frontend, 30 ES6 modules talking to 150+ API endpoints. It was the closest I'd gotten, but I was still maintaining two separate applications — a backend and a frontend that both needed to know about every feature. Every change meant writing it twice.

That's when Claude suggested HTMX. The pitch was simple: what if the server just sent HTML most of the time? You still have API endpoints where they make sense — autosave, async operations — but instead of building an entire JavaScript application to consume a JSON API, Flask renders the page and HTMX handles the dynamic parts. So we rebuilt the whole thing, again. But this time, V3 was strong and we didn't have to keep re-working code over and over. I built AI guard rails as we went, standardizing how we built tools and documented code. I'm actually proud of what Scribere Studio is now, and it just works.

So What Does HTMX Actually Look Like?

Here's what the code actually looks like.

Live Search + Filters

Users search scenes and filter by book/status. As they type, the outline rebuilds. In React this would be a whole thing — useState, useEffect, a debounce hook, loading states. With HTMX:

<input type="text"
       id="outline-search"
       name="search"
       placeholder="Search scenes..."
       value="{{ current_search }}"
       hx-get="{{ url_for('planning.partial_outline', series_id=series.id) }}"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#outline-content"
       hx-include="#outline-filter-form">
Enter fullscreen mode Exit fullscreen mode

That's it. User types, waits half a second, server filters, HTMX swaps in the new HTML. The server is the source of truth and you're not wrestling with state management on the client side.

Inline Auto-Save

Writers edit book elements like forewords, prologues, and epilogues. Everything autosaves as they type — they just see a little "Saving..." that flips to "Saved" and keep working.

<input type="text"
       id="element-title"
       class="be-title-input"
       value="{{ element.title or '' }}"
       onchange="autoSaveElement()">

<span class="be-status-indicator" id="save-status">
    <span class="material-symbols-outlined">check_circle</span>
    Saved
</span>
Enter fullscreen mode Exit fullscreen mode
let autoSaveTimeout;

elementEditor.on('update', () => {
    clearTimeout(autoSaveTimeout);
    document.getElementById('save-status').innerHTML =
        '<span class="material-symbols-outlined">hourglass_empty</span> Saving...';
    autoSaveTimeout = setTimeout(autoSaveElement, 1500);
});

function autoSaveElement() {
    const title = document.getElementById('element-title')?.value || '';
    const content = elementEditor.getHTML();

    fetch(`/api/book-element/${elementId}/autosave`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({title, content})
    })
    .then(res => res.json())
    .then(data => {
        if (data.success) {
            document.getElementById('save-status').innerHTML =
                '<span class="material-symbols-outlined">check_circle</span> Saved';
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

The React equivalent would need controlled inputs, form state, async handlers, loading indicators — a lot of ceremony for something that's basically just "save what I typed."

The Stuff That Sucks

It's still a lot of work. HTMX made the architecture simpler, but simpler doesn't mean easy. You're still building a real application, and there's no component library to lean on. Need a modal? Build it. Need a drag-and-drop card system? Figure it out. React has an ecosystem for that stuff. HTMX has you and whatever you can wire together.

And honestly? The whole reason I built this thing was so I could write my books. I've spent so much time building the tool that the writing took a back seat. That's not HTMX's fault, that's just what happens when a side project swallows your life. But it's worth saying out loud because I think a lot of developers fall into that trap — you start building something to solve a problem and the building becomes the problem.

The good news is I'm in alpha now. I'm looking for testers, and I'm going to take a step back from development and actually use the thing for what I built it for. Book 2 is waiting, and Scribere is absolutely going to make it easier to write than book 1 was.

The Numbers

I went and actually counted. The SPA version had about 16,000 lines of JavaScript. The HTMX version has under 10,000, and honestly most of that is the text editor bundle. HTMX itself is 47 KB. The app does way more than it used to — more routes, more features, more database models — but I cut 40% of the JavaScript out. All that complexity moved to the server side, where I can debug it with print statements like a normal person instead of staring at the browser console.

Would I Do It Again?

Yeah. I'm already planning the next round — 3.9 Beta with a bunch of features and tweaks, and if all goes well, a full 4.0 release to the public. I can't imagine going back to maintaining a JavaScript SPA on top of everything else. HTMX let me build something real, by myself, with a full-time job and a five-book series I'm supposed to be writing. That's the whole pitch. Check it out if you want to see it in action at scribere.app, and I'm always happy to talk about it.

Top comments (0)