The problem: swapping part of a page is harder than it should be
You click a "Load more" button. Behind the scenes, your app fetches some HTML and shoves it into a <div>. Simple, right?
Except it never stays simple. You hit edge cases with orphaned event listeners, focus jumping around, scripts that mysteriously don't execute, and weird flashes when you replace a chunk of DOM.
I ran into this again last month while refactoring a dashboard. The team had jQuery doing partial swaps for the better part of a decade and finally wanted out. The "modern" alternatives all looked great in demos. Each one came with its own quirks.
Why the web makes this awkward
HTML, at its core, doesn't have a primitive for "replace this region with the contents of that response." You can navigate the whole page (<a href>, <form action>), or you can call fetch() and write the result into innerHTML yourself. There's no middle ground built into the platform.
That gap is why entire categories of tools exist:
- HTMX turns attributes into AJAX swaps
-
Turbo (Hotwire) wraps regions in
<turbo-frame>and intercepts navigation - React/Vue/Svelte rebuild the page from a virtual representation
- Unpoly and friends do progressive enhancement around forms
Each of them is solving a problem the browser itself never tried to solve.
A quick tour of the pain
Here's the typical "just fetch and replace" approach:
// Replace #content with HTML from the server
async function loadFragment(url) {
const res = await fetch(url);
const html = await res.text();
document.querySelector('#content').innerHTML = html;
}
Looks fine. But:
- Inline
<script>tags inhtmlwon't run (a long-standing quirk ofinnerHTML). - Any event listeners attached to the old
#contentchildren are gone. - If the user had focus inside the replaced region, it's lost.
- Custom elements may re-run constructors in surprising ways.
After getting bitten by all of these in production, most teams reach for a library.
How HTMX solves it today
HTMX handles a lot of this for you with declarative attributes. The swap target and strategy live in the markup:
<button
hx-get="/api/comments?page=2"
hx-target="#comments"
hx-swap="beforeend"
>
Load more
</button>
<ul id="comments">
<!-- existing items -->
</ul>
It handles script execution, optional out-of-band swaps, focus preservation via hx-preserve, and history integration. Worth reading the official docs if you haven't.
Turbo Frames, briefly
If you're in the Rails/Hotwire world, <turbo-frame> does similar work by wrapping regions and intercepting links inside them:
<turbo-frame id="comments" src="/posts/42/comments">
Loading…
</turbo-frame>
Both approaches work. Both require shipping a library. Both reinvent things the browser arguably should handle itself.
What Chrome is reportedly exploring
According to the Chrome developers blog post on declarative partial updates, there's an early-stage proposal to give the platform a native way to express "replace this region with what comes back from the server." I haven't shipped anything against the proposal yet — at the time of writing it reads as an explainer, not a stable API — but the direction is interesting.
The general idea, as I read it, is that you describe the swap declaratively in markup and the browser does the fetch and DOM update for you. Think of it as the platform absorbing patterns that libraries like HTMX have demonstrated work well.
If you want to follow along officially, the right places to watch are:
- The Chrome status entries for the proposal
- The WHATWG repos where standardization discussion happens
- The Web Platform Tests repo once an implementation lands
I would resist building anything load-bearing on a proposal-stage API. The shape will almost certainly shift.
What to do in the meantime
If you're feeling the pain today, here's the order I'd try things in:
-
Use the simplest tool that fits. If you only need partial updates in two places, hand-rolled
fetchplusreplaceChildrenis fine. - Reach for HTMX or Turbo if you're doing this everywhere. Don't build your own framework. I've watched two teams try; both ended up with a worse HTMX.
- Keep server-rendered HTML as the source of truth. Returning JSON and reconstructing markup on the client is what got us into the SPA mess in the first place.
Here's a safer vanilla pattern I lean on when a library would be overkill:
async function swapFragment(targetSelector, url) {
const res = await fetch(url, { headers: { Accept: 'text/html' } });
if (!res.ok) throw new Error(`Fragment fetch failed: ${res.status}`);
const html = await res.text();
const template = document.createElement('template');
template.innerHTML = html; // parsed in an inert context, not executed
const target = document.querySelector(targetSelector);
// replaceChildren preserves more state than reassigning innerHTML
target.replaceChildren(...template.content.childNodes);
}
A few notes on why this is less awful than the naive version:
-
<template>parses HTML in an inert context, so custom elements don't upgrade twice. -
replaceChildrenis generally kinder to scroll position and selection than overwritinginnerHTML. - We still need to manually re-run any inline scripts the server sends. Honestly, I just don't.
Prevention tips so this doesn't haunt you again
A handful of habits that have saved me grief:
- Pick one swap strategy per project. Mixing HTMX, Turbo, and hand-rolled fetches makes debugging miserable.
- Test focus, scroll, and selection as part of normal QA — not just "did the right content appear."
- Treat the response shape as a contract. If one endpoint returns JSON for some callers and HTML for others, you've doubled your test surface.
- Watch the standards work. If a declarative API does land in browsers, your migration story will be much smoother if your current solution is markup-first rather than imperative JS scattered across the codebase.
Wrapping up
Partial page updates are one of those problems that look tiny from the outside and turn into a tar pit on the inside. Libraries have papered over the gap for years and done a respectable job. If browsers eventually expose a native primitive for this, a lot of glue code will go away. Until then: pick a sane tool, keep markup as the source of truth, and don't roll your own. I've tried. It's not worth it.
Top comments (0)