Adapters
An adapter controls how placeholders and patches are encoded in the HTML, in the markup a specific client (a library, the browser, a CDN) recognises and acts on.
Vincle ships five built-in adapters. Import from @vincle/flow/adapters:
import { TurboAdapter, // Turbo Streams: <turbo-frame> / <turbo-stream>, morph on Turbo >= 8 HtmxAdapter, // HTMX: hx-get / hx-swap-oob, morph on htmx >= 4 NativeAdapter, // <template data-for> + inline polyfill, all 5 positions WebPlatformAdapter, // WICG declarative partial updates, native support required EsiAdapter, // CDN edge composition via esi:include} from "@vincle/flow/adapters";Choosing an adapter#
| Adapter | Client runtime | Streaming | Merge types | Best for |
|---|---|---|---|---|
| NativeAdapter | ~1.5 kB polyfill, gzipped (auto-injected) | all 5 positions | General purpose, no extra JS | |
| TurboAdapter | Hotwire Turbo | all 6 | Rails / Hotwire stack | |
| HtmxAdapter | HTMX | all 6 | Django / Laravel / any backend | |
| WebPlatformAdapter | None (native browser support required) | replace only | Browsers with declarative partial updates | |
| EsiAdapter | None (CDN-level) | replace only | Varnish / CDN edge composition |
morph needs a diffing client (Turbo, HTMX); an adapter refuses a merge type it
does not support.
NativeAdapter#
Recommended for most applications. It uses the
WICG Declarative Partial Updates
format, with a polyfill injected automatically when fragments or active
<template src> / <template for> elements are present. Inert templates and
pages without deferred content do not require it.
Use Native for both SSG and streaming while native browser support is incomplete.
Native protects templates with data-for, so browsers that implement patching
but not src cannot consume them before the polyfill. It handles both comment
and processing-instruction markers.
SSG loads the emitted fragment files. Streaming consumes complete inline patch templates and also handles external includes arriving in the page. External responses are buffered; failed requests preserve fallback content.
Once target browsers support both WICG patching and Fragment Include, switch the
render call to WebPlatformAdapter. Keep the same JSX and generated URLs and
use merge="replace", which both adapters support. WebPlatform emits standard
for attributes and adds no JavaScript.
WebPlatformAdapter#
The standard WICG format with no polyfill: the initial HTML holds a named range with
fallback content, and a matching <template for="…"> later in the response
replaces it as the browser parses it.
<?start name="comments">Loading comments…<?end><!-- Later in the same HTML stream: --><template for="comments"><p>First comment</p></template>For a static-mode <Defer>, Vincle instead emits an external include:
<?start name="comments">Loading comments…<?end><template for="comments" src="/fragments/comments.html"></template>emitFragments writes raw HTML at that URL, with no <template> wrapper.
This follows the experimental
Fragment Include proposal.
You can also render <template src="/partials/header.html" for="" /> directly
in JSX for an in-place include. Importing Flow also enables buffer, sanitize, crossorigin,
and referrerpolicy as well. Without native support, these includes remain inert.
The Native polyfill supports targeted and in-place includes, but buffers external
responses and does not implement the proposal’s sanitization API.
It needs native browser support; Google's
Declarative partial updates article
covers the motivation and browser support. For the polyfill and the other merge
positions, use NativeAdapter.
TurboAdapter#
Encodes placeholders as <turbo-frame> elements and patches as
<turbo-stream> elements, for Hotwire Turbo.
// Placeholder in shell:// <turbo-frame id="comments">Loading…</turbo-frame>
// Patch:// <turbo-stream action="replace" target="comments">// <template>…</template>// </turbo-stream>
// Patch, merge="morph" (Turbo >= 8):// <turbo-stream action="replace" method="morph" target="comments">// <template>…</template>// </turbo-stream>HtmxAdapter#
Encodes placeholders (with hx-get when the fragment has a URL, in static mode)
and patches as hx-swap-oob elements, for HTMX. For request negotiation, see
negotiateHtmx.
// Placeholder in shell:// <div id="comments" hx-get="/_fragments/comments" hx-trigger="load" hx-swap="outerHTML">Loading…</div>
// Patch:// <div id="comments" hx-swap-oob="outerHTML">…</div>
// Patch, merge="morph" (htmx >= 4):// <div id="comments" hx-swap-oob="outerMorph">…</div>EsiAdapter#
Encodes placeholders as <esi:include> elements; an ESI-capable CDN or reverse
proxy (Varnish, Akamai, Fastly) fetches and inserts the content. Static only
(streaming: false), replace only: the origin emits one HTML template and the
edge fills in the dynamic parts.
Custom adapters#
Build your own with createAdapter from @vincle/flow/adapters:
| Export | Purpose |
|---|---|
Placeholder |
Renders the shell placeholder for a deferred fragment |
Patch |
Wraps a resolved fragment for injection into the DOM |
Frame |
Wraps a static fragment file for SSG output |
capabilities |
Declares supported merge types and streaming support |
transformShell |
(Optional) Post-processes the shell before it enters the stream |
import { createAdapter } from "@vincle/flow/adapters";
const MyAdapter = createAdapter({ Placeholder: ({ id, children }) => <div data-defer={id}>{children}</div>, Patch: ({ id, children, merge }) => ( <div data-patch={id} data-merge={merge}> {children} </div> ), Frame: ({ id, children }) => <div data-frame={id}>{children}</div>, capabilities: { streaming: true, merges: ["replace", "append"] },});An adapter does not implement the streaming wire: renderToStream owns it, and
capabilities.streaming: true only declares the adapter can stream.
transformShell can inject a client runtime or modify the shell HTML. It
receives a ShellContext, not the full flow context: just
ctx.fragments.size, so you can inject scripts only when fragments are
actually pending.