Skip to content

Craftit Lifecycle Best Practices

Prefer Setup-Scope Reactivity

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

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

define('counter-title', {
  setup() {
    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.

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

define('my-tabs', {
  setup(_props, { 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, onElement, ref } from '@vielzeug/craftit';

define('focus-input', {
  setup() {
    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 one-liner host helpers for individual bindings and host.bind(...) for grouped bindings.

ts
import { computed, define, handle, html, signal } from '@vielzeug/craftit';

define('toggle-host', {
  setup(_props, { host }) {
    const open = signal(false);
    const expanded = computed(() => String(open.value));

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

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

    // handle() auto-registers cleanup when called inside setup()/scope.run().
    // If you call it elsewhere, keep the returned cleanup function and dispose it manually.

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

Pick the Right Cleanup Primitive

  • Use onCleanup(fn) for component-owned teardown.
  • Use onElement() for per-element teardown.
  • Return cleanup from onMounted() when cleanup belongs to mount-time setup.