Micra.js LLM Guide

This file helps LLMs generate correct Micra.js code. Read it before suggesting code that uses this library.

Quick decision: Micra.js vs alternatives

SituationUse
SSR page (Rails, Laravel, Django) + small interactivityMicra.js
Full SPA with client-side routingReact / Vue
Bundle size must be < 10 KBMicra.js
<script> tag in existing HTML, no build stepMicra.js
Complex client state + Redux/Zustand patternsReact
Team already invested in React ecosystemReact

Anti-patterns LLMs gravitate to (DO NOT)

These are the most common ways LLMs misuse Micra. The “anti-pattern” code on the left works but defeats the purpose of the library. Always use the right column.

Wrong artifact type in Claude / ChatGPT canvas / v0 / Replit AI

This is the most insidious failure because it happens before code generation: the tool picks the wrong artifact type, then writes the requested logic in the wrong framework.

Don’t:

User: “Build a todo on Micra.js — here’s the recipe.” Claude: creates application/vnd.ant.react artifact, writes a React todo, waves at the Micra recipe as “irrelevant — this is React env”

Do:

Claude: creates application/vnd.ant.html artifact, includes <script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js">, writes Micra components per the recipe

These tools default to React when asked “build a UI”. When the user has indicated Micra (via prompt, recipe, project file, or explicit ask), that signal overrides the React default. The HTML artifact type exists precisely for libraries like Micra. Do NOT silently translate Micra patterns into React because the framework auto-routed you there — switch the artifact type.

Same applies to:

List rendering

Don’t roll your own DOM:

document.getElementById("list").innerHTML = items
  .map((i) => `<li>${i.name}</li>`)
  .join("");
// ...or...
items.forEach((item) => {
  const el = document.createElement("div");
  el.textContent = item.name;
  list.appendChild(el);
});

Do use data-each:

<template data-each="items" data-key="id">
  <li data-text="item.name"></li>
</template>

Derived values (counts, totals, filters)

Don’t store them as state fields synced manually:

state: { todos: [], totalCount: 0, hasDone: false, filteredCount: 0 }
updateComputeds() {
  this.state.totalCount = this.state.todos.length          // ← spaghetti
  this.state.hasDone = this.state.todos.some(t => t.done)  // ← can drift
}

Do make them methods, call them from directives:

state: { todos: [] }                                         // single source of truth
totalCount() { return this.state.todos.length }
hasDone()    { return this.state.todos.some(t => t.done) }
filtered()   { return this.state.todos.filter(...) }
<span data-text="totalCount()"></span>
<button data-if="hasDone()">Clear done</button>

Event handlers

Don’t use addEventListener inside a render-like method:

createItem(item) {
  const el = document.createElement('div')
  el.addEventListener('click', () => this.toggle(item.id))  // ← leaks on destroy
  return el
}

These listeners are NOT tracked by Micra and survive instance.destroy(), causing memory leaks and “zombie” handlers.

Do use @event / data-on — Micra tracks and cleans them up:

<div @click="toggle" data-bind="data-id:item.id">...</div>
toggle(e) {
  const id = e.currentTarget.dataset.id
  // ...
}

After a state mutation

Don’t manually trigger a re-render:

addTask() {
  this.state.todos = [...this.state.todos, x]
  this.renderList()        // ← Micra already re-renders
  this.updateComputeds()   // ← derived methods recompute on read
  this.refresh()           // ← no such concept
}

Do only side effects (persistence, network, analytics):

addTask() {
  this.state.todos = [...this.state.todos, x]  // ← Micra re-renders
  this.save()                                   // ← side effect OK
}

Nested paths in data-model

Dot-paths are supporteddata-model reads and writes through the path, reconstructing the nested object immutably:

<input data-model="user.email" />
<!-- reads & writes state.user.email -->
<input data-model="filters.query" />
<!-- reconstructs state.filters -->
Micra.define("profile", {
  state: { user: { email: "" } },
});

⚠️ Bracket/computed paths are NOT parsed — data-model="filters[0]" is a literal flat key. Use dot notation (filters.0 also works for array indices). To set a nested value from JS, use this.set('user.email', x) (or replace the top-level object yourself).

Timers and external listeners in onCreate

Don’t forget to clean up in onDestroy:

onCreate() {
  setInterval(() => this.tick(), 1000)         // ← never cleaned up
  document.addEventListener('click', this.outside)  // ← never removed
}

Do keep references and clean up:

onCreate() {
  this._timer = setInterval(() => this.tick(), 1000)
  this._outside = e => { /* ... */ }
  document.addEventListener('click', this._outside)
},
onDestroy() {
  clearInterval(this._timer)
  document.removeEventListener('click', this._outside)
}

(Subscriptions made with this.on('event', fn) are cleaned up automatically. @event / data-on / data-model listeners too — these are tracked by Micra. Only manual addEventListener / setInterval / setTimeout need explicit cleanup.)

React-style component model

Don’t export a function:

function Counter() {
  /* ... */
} // not a thing in Micra
const Counter = ({ count }) => <span>{count}</span>; // not a thing

Do define + mount:

Micra.define("counter", {
  state: { count: 0 },
  inc() {
    this.state.count++;
  },
});
Micra.start();
<div data-component="counter">
  <span data-text="count"></span>
  <button @click="inc">+</button>
</div>

Imports

import React, import { ref } from 'vue', import Alpine, htmx, etc.

✅ Only Micra:

import * as Micra from "micra.js";
// or via CDN: <script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js"></script>
// Then use the global Micra.

CDN URL — use jsDelivr, not unpkg

Don’t:

<script src="https://unpkg.com/micra.js/dist/micra.min.js"></script>

This silently fails in Claude artifacts, ChatGPT canvas, and most sandboxed AI environments — their Content Security Policy blocks unpkg.com.

Do:

<script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js"></script>

cdn.jsdelivr.net is in the script-src allowlist of every major AI runtime and auto-mirrors every npm package, so the URL is equivalent.

Common mistakes LLMs make

Wrong: React patterns

// DON'T — this is React, not Micra.js
import React, { useState } from "react";
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Correct: Micra.js pattern

// DO — define + data-component + data-text + @click
Micra.define("counter", {
  state: { count: 0 },
  increment() {
    this.state.count++;
  },
});
Micra.start();
<div data-component="counter">
  <span data-text="count"></span>
  <button @click="increment">+</button>
</div>

Wrong: function components

// DON'T — Micra.js has no function component model
const Counter = () => { ... }

Wrong: importing React or Vue

// DON'T
import React from "react";
import { createApp } from "vue";

Correct: importing Micra.js

// DO — ESM
import * as Micra from "micra.js";

// DO — CDN (global)
// <script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js"></script>
// Then use window.Micra or just Micra

Wrong: mutating state directly without proxy

// This works because state is a Proxy — don't bypass it
const raw = { count: 0 };
this.state = raw; // DON'T replace the proxy

Correct: assign properties on the existing proxy

this.state.count++        // triggers re-render
this.state.items = [...]  // triggers re-render

State rules

// DON'T
this.state.user.name = "Alice"; // nested mutation — not tracked

// DO
this.state.user = { ...this.state.user, name: "Alice" };

SSR props

Read data-* attributes set by the server with this.prop():

<div data-component="profile" data-user-id="42" data-username="alice"></div>
Micra.define("profile", {
  state: {},
  onCreate() {
    const id = this.prop("user-id"); // '42' (string)
    const name = this.prop("username", "anon"); // 'alice'
  },
});

List rendering

<ul data-component="list">
  <template data-each="items" data-key="id">
    <li data-text="name"></li>
  </template>
</ul>
Micra.define("list", {
  state: {
    items: [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
    ],
  },
});

Event bus (cross-component communication)

// Component A emits
Micra.emit("cart:updated", { count: 3 });

// Component B listens
Micra.on("cart:updated", ({ count }) => {
  this.state.cartCount = count;
});

// Manual unsubscribe (rare — prefer this.on() inside components)
Micra.off("cart:updated", handler);

Component-scoped versions auto-unsubscribe on destroy:

onCreate() {
  this.on('cart:updated', ({ count }) => {
    this.state.cartCount = count
  })
}

Supported since 2.5

Things Micra does NOT support

data-if vs data-show

Since 2.0 these are different directives — pick deliberately:

<!-- modal — should not exist in DOM when closed -->
<dialog data-if="modalOpen">…</dialog>

<!-- dropdown panel — fine to leave in DOM, just hidden -->
<div data-show="open" class="dropdown">…</div>

When the user says “show/hide”, default to data-show. When the user says “render conditionally”, “only when X”, “branch”, default to data-if.

DOM refs

<div data-component="editor">
  <canvas data-ref="canvas"></canvas>
</div>
Micra.define("editor", {
  state: {},
  onCreate() {
    const ctx = this.refs.canvas.getContext("2d");
  },
});

Fetch helper

this.fetch() wraps window.fetch with JSON defaults:

async loadData() {
  const data = await this.fetch('/api/items')
  this.state.items = data
}

Direct mount (no data-component)

const instance = Micra.mount("#my-element", {
  state: { open: false },
  toggle() {
    this.state.open = !this.state.open;
  },
});

Returns the instance or null if the selector matches nothing.

Security model

Directive expressions are parsed and interpreted by a built-in evaluator — no new Function, no eval, so Micra works under a strict Content-Security-Policy (default-src 'self'). Identifiers resolve to: state keys → instance methods → a small whitelist of globals (Math, JSON, Date, String, Number, Boolean, Array, Object, parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined).

Everything else — window, document, fetch, eval, setTimeout, constructor, __proto__, … — is unreachable and returns undefined (by construction, not shadowing). Member access also blocks __proto__ / constructor / prototype, so data-text="constructor.constructor('alert(1)')()" is blocked.

Two things this does NOT do:

  1. data-html is still XSS-prone. It writes innerHTML directly. Use data-text if you can, or register a sanitizer once: Micra.config({ sanitize: DOMPurify.sanitize }) — it runs on every data-html value.
  2. It’s not a full sandbox. Directive markup itself must be trusted. Methods you put on the component can do anything (they’re your code).

Destroy / cleanup

const instance = Micra.mount('#widget', { ... })
instance.destroy()  // unmounts, removes event listeners, runs onDestroy