Overview

xRaw(Class, props?) lives in mates-raw — a standalone package whose only runtime dependency is lit-html@3.2.1. It constructs the class, commits the class’s html field into a child slot once, then calls setup() on a microtask after the DOM (and refs) are ready. Parent re-renders never touch the island again.

Import from mates-raw for a tiny vanilla or React island. Import from mates when you already have the framework — mates re-exports the same API and skips setup() during SSR. Use $ / atoms only from mates.

You do not get component hooks (on, onKeyDown, onMount, …), auto atom subscriptions, or auto cleanup. Subscribe and unbind yourself; destroy? runs only on disconnect.

xRaw vs x()

Aspect
xRaw
x()
Host mates-raw child slot x() + RenderScheduler
Updates Mount once — omit onUpdate Re-runs template on tracked reads
Hooks None — use $(ref).on(...) on / onKeyDown / lifecycle hooks
Atoms Manual __subscribe + $ patch Auto-tracked in the template
Cleanup Manual in destroy() Hooks / effects auto-clean on unmount

Standalone — mates-raw

No mates scheduler, no $. Patch the DOM in setup(). Same class works inside React via mates-raw/react.

import { html, ref, setRef, renderRaw } from 'mates-raw';

class Counter {
  declare props: { label?: string };
  button = ref<HTMLButtonElement>();
  n = 0;

  html = html`
    <button ${setRef(this.button)}>
      ${this.props.label ?? 'count'} is 0
    </button>
  `;

  setup() {
    this.button.value!.onclick = () => {
      this.n++;
      this.button.value!.textContent =
        `${this.props.label ?? 'count'} is ${this.n}`;
    };
  }

  destroy() {
    if (this.button.value) this.button.value.onclick = null;
  }
}

renderRaw(Counter, document.getElementById('app')!, { label: 'count' });

React island

Optional peer. Mount-once: later React prop changes do not remount. Put the returned ref on a host element.

import { useRaw } from 'mates-raw/react';

function Widget() {
  const host = useRaw(Counter, { label: 'count' });
  return <div ref={host} />;
}

Counter with $ patches

Mount once, then update text via $(ref).text — no x() re-renders.

import { html, ref, setRef, $, xRaw, renderApp } from 'mates';

class Counter {
declare props: { label?: string };
button = ref();
n = 0;

html = html`
<button class="btn-primary" ${setRef(this.button)}>
${this.props.label ?? 'count'} is 0
</button>
`;

setup() {
$(this.button).on('click', () => {
this.n++;
$(this.button).text(`${this.props.label ?? 'count'} is ${this.n}`);
});
}

destroy() {
$(this.button).off('click');
}
}

const App = () => () => html`
<div class="m-col m-gap m-items-center">
${xRaw(Counter, { label: 'count' })}
</div>
`;
renderApp(App, document.getElementById('app'));

Manual atom subscription

Atoms do not auto-track in Raw. Subscribe in setup, patch with $, unsubscribe in destroy.

import { html, ref, setRef, $, atom, xRaw, renderApp } from 'mates';

const count = atom(0);

class CountView {
declare props: Record<string, never>;
el = ref();
unsub;

html = html`<p class="center" ${setRef(this.el)}>count is 0</p>`;

setup() {
// No auto-tracking — subscribe yourself, then patch with $.
this.unsub = count.__subscribe((n) => {
$(this.el).text('count is ' + n);
});
$(this.el).on('click', () => count.set((n) => n + 1));
}

destroy() {
this.unsub?.();
$(this.el).off('click');
}
}

const App = () => () => html`
<div class="m-col m-gap m-items-center">
<p class="muted">Click the line — atom updates are manual</p>
${xRaw(CountView)}
</div>
`;
renderApp(App, document.getElementById('app'));