Overview

A lazy component is an async function (or Promise/thenable) that resolves to a Component — or a module with a default export that is a Component. Pass it to x() exactly like an eager component: x(async () => import('./Heavy'), props).

When x() receives an async loader, Mates calls isAsyncValue, tears down any currently mounted view immediately (so the old page does not linger), then resolveAsyncValueunwrapModule and assigns the sync Component. While loading, the host renders empty.

isXViewTemplate(value) returns true for TemplateResults produced by x() / view() / template() (branded with a shared Symbol). Useful for SSR helpers and tooling that must distinguish component embeddings from plain html results.

Eager vs lazy vs manual atom swap

Pattern
API
When
Eager x(Comp, props) Default — component already in the bundle.
Lazy (built-in) x(async () => import(…), props) Code-split at the import boundary. Same API as eager x().
Manual swap atom + x(Comp(), {}) Store a resolved constructor yourself — fine for demos; prefer LazyComponent for real splits.
Router component: async () => import(…) Same lazy resolution path as x().

Lazy load with x()

Click Load chart — x(loadChart) detects the async function, waits, unwraps { default: Chart }, then mounts with props. In production, use async () => import('./Chart').

import { html, atom, x, renderApp } from 'mates';

const Chart = (propsFn) => () => html`
<div class="card card-body">
<p class="m-0">Chart · ${propsFn().label}</p>
</div>
`;

// Simulates: async () => import('./Chart')
const loadChart = async () => {
await new Promise((r) => setTimeout(r, 500));
return { default: Chart };
};

const App = () => {
const show = atom(false);
return () => html`
<div class="m-col m-gap">
<button class="btn-primary" @click=${() => show.set(true)}>
Load chart
</button>
${show()
? x(loadChart, { label: 'Revenue' })
: html`<p class="muted m-t-5">Not loaded yet</p>`}
</div>
`;
};

renderApp(App, document.getElementById('app'));

Swap lazy views + isXViewTemplate

Switching A/B tears down the previous view immediately and starts a new resolve. isXViewTemplate(x(...)) is true — plain html`...` is false.

import { html, atom, x, renderApp, isXViewTemplate } from 'mates';

const A = () => () => html`<p class="m-0">Panel A</p>`;
const B = () => () => html`<p class="m-0">Panel B</p>`;

const loadA = async () => {
await new Promise((r) => setTimeout(r, 300));
return A;
};
const loadB = async () => {
await new Promise((r) => setTimeout(r, 300));
return B;
};

const App = () => {
const which = atom('a');

return () => {
const viewTpl = which() === 'a' ? x(loadA) : x(loadB);
return html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button @click=${() => which.set('a')}>A</button>
<button class="btn-primary" @click=${() => which.set('b')}>B</button>
</div>
<p class="muted m-t-5">
isXViewTemplate: ${String(isXViewTemplate(viewTpl))}
</p>
${viewTpl}
</div>
`;
};
};

renderApp(App, document.getElementById('app'));