Skip to content

Craft Lifecycle Best Practices

Prefer Setup-Scope Reactivity

Most component logic should live directly in setup() using signals and effect().

ts
import { define, html, signal } from '@vielzeug/craft';

define('counter-title', {
  setup(_props, { effect }) {
    const count = signal(0);

    effect(() => {
      document.title = 'Count: ' + count.value;
    });

    return html`<button @click=${() => count.value++}>${count}</button>`;
  },
});

Run DOM Initialization with onMounted()

Use onMounted(fn) for DOM-dependent initialization. Multiple onMounted() calls are supported and run in registration order. Each callback is error-isolated.

ts
import { define, html, ref, signal } from '@vielzeug/craft';

define('my-tabs', {
  setup(_props, { onMounted, slots }) {
    const activeTab = signal(0);
    const containerRef = ref<HTMLElement>();

    onMounted(() => {
      const panels = slots.elements('panels').value;
      if (panels.length > 0 && activeTab.value >= panels.length) {
        activeTab.value = 0;
      }
    });

    return html`
      <div ref=${containerRef}>
        <div role="tablist"><slot name="tabs"></slot></div>
        <div role="tabpanel"><slot name="panels"></slot></div>
      </div>
    `;
  },
});

Use onElement() for Ref-Driven Effects

onElement(ref, callback) is ideal for imperative DOM logic tied to a specific referenced element.

ts
import { define, html, ref } from '@vielzeug/craft';

define('focus-input', {
  setup(_props, { onElement }) {
    const inputRef = ref<HTMLInputElement>();

    onElement(inputRef, (input) => {
      input.focus();

      const onKeydown = (e: KeyboardEvent) => {
        if (e.key === 'Escape') input.blur();
      };

      input.addEventListener('keydown', onKeydown);

      return () => input.removeEventListener('keydown', onKeydown);
    });

    return html`<input ref=${inputRef} />`;
  },
});

Keep Host Wiring Explicit

Use bind() from the setup context for host bindings.

ts
import { define, html, signal } from '@vielzeug/craft';

define('toggle-host', {
  setup(_props, { bind, onEvent }) {
    const open = signal(false);

    bind({
      attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },
      class: { 'is-open': open },
      on: { click: () => (open.value = !open.value) },
    });

    onEvent(window, 'keydown', (e) => {
      if (e.key === 'Escape') open.value = false;
    });

    return html`<slot></slot>`;
  },
});

Pick the Right Cleanup Primitive

  • Use ctx.onCleanup(fn) for component-owned teardown (intervals, WebSockets, subscriptions).
  • Use ctx.onElement() for per-element teardown tied to a specific ref.
  • Return a cleanup function from ctx.onMounted() when cleanup belongs to mount-time initialization.
  • Use ctx.onEvent() for event listeners that should auto-cleanup on disconnect.