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
| Situation | Use |
|---|---|
| SSR page (Rails, Laravel, Django) + small interactivity | Micra.js |
| Full SPA with client-side routing | React / Vue |
| Bundle size must be < 10 KB | Micra.js |
<script> tag in existing HTML, no build step | Micra.js |
| Complex client state + Redux/Zustand patterns | React |
| Team already invested in React ecosystem | React |
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.reactartifact, writes a React todo, waves at the Micra recipe as “irrelevant — this is React env”
✅ Do:
Claude: creates
application/vnd.ant.htmlartifact, 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:
- ChatGPT canvas: pick “HTML” output, not “React”.
- v0 / Bolt: pick “Plain HTML” template, not Next.js.
- Replit AI: pick “HTML/CSS/JS” project, not React.
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 supported — data-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
stateis declared as a plain object in the definition and becomes a reactiveProxyon mount- Assigning any property on
this.stateschedules a batched re-render - Nested objects are NOT auto-tracked — replace the whole nested value to trigger a render:
// 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" },
],
},
});
data-eachiterates overstate[expression]data-keymust be a unique property — enables keyed diffing (stable node identity on reorder). Withoutdata-key, Micra still reuses row nodes positionally, but reorders count as “every row changed”- Inside the template,
data-text,data-bind, etc. reference item properties directly
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
- Key modifiers —
@keydown.enter,.escape,.tab,.space, arrows,.delete, and system keys.ctrl/.shift/.alt/.metagate the handler. Combine:@keydown.ctrl.enter. (Plus.prevent/.stop/.self.) - Dot-paths in
data-model—data-model="filters.search"reads and writesstate.filters.search. Same for the path setterthis.set('filters.search', x). - Call expressions in
@event—@click="select(item.id)",@input="set($event.target.value)".
Things Micra does NOT support
- Computed/bracket paths —
data-model="filters[0]"is a literal key; use dot notation. - Assignments /
new/ arrow functions in directive expressions — move that logic into a method.
data-if vs data-show
Since 2.0 these are different directives — pick deliberately:
data-if="expr"— unmounts the element from the DOM when falsy and re-inserts it when truthy. Use for conditional sections, modals, branches that shouldn’t exist in the tree when off.data-show="expr"— togglesstyle.display, element stays in the DOM. Use for frequently-toggled visibility (dropdowns, tooltips, accordions).
<!-- 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:
data-htmlis still XSS-prone. It writesinnerHTMLdirectly. Usedata-textif you can, or register a sanitizer once:Micra.config({ sanitize: DOMPurify.sanitize })— it runs on everydata-htmlvalue.- 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