Skip to content
Vincle

Loading…

    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#

    AdapterClient runtimeStreamingMerge typesBest for
    NativeAdapter~1.5 kB polyfill, gzipped (auto-injected)all 5 positionsGeneral purpose, no extra JS
    TurboAdapterHotwire Turboall 6Rails / Hotwire stack
    HtmxAdapterHTMXall 6Django / Laravel / any backend
    WebPlatformAdapterNone (native browser support required)replace onlyBrowsers with declarative partial updates
    EsiAdapterNone (CDN-level)replace onlyVarnish / 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.