Overview

Experimental — not ready for production. xRaw(Class, props?) is part of mates (the former standalone mates-raw package has been merged in and removed). 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 everything from mates — there is no separate package to install. Use $ / atoms from mates in setup(); the raw island itself gets no reactivity.

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 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

Same class, one import — from mates

No separate package, no extra install. The raw core ships inside mates, so the identical class works with renderRaw outside an app or xRaw inside one.

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

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' });

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'));