A minimalist reactive component library with support for state, props, named slots, refs, DOM diffing, shared context, and declarative event bindings.
createComponent()
with internalstate
,setState
, andrefs
- Props are read-only (passed in by parent)
- Declarative rendering using template strings or render functions
- Named and default slots (
<slot name="...">
anddata-slot="..."
support) - Support for
this.refs
and lazythis.ref(name)
inside components - Lifecycle hooks:
onMount
,onUnmount
,onBeforeMount
,onBeforeUnmount
,onUpdate
- Diffing DOM updates for performance with
data-key="..."
- Keyed list rendering with
renderList()
for efficient updates - DOM caching when
render()
returnsnull
- Declarative event binding via
on
option (e.g."click .btn"
) anddata-action
- Arguments support via
data-args
for cleaner templates - Built-in
context
pub/sub withshared()
stores - Full unit test coverage
npm i @magnumjs/micro-ui
import { createComponent } from "@magnumjs/micro-ui";
const ClickCounter = createComponent({
state: {
count: 0,
},
render() {
this.handleClick = () => {
this.setState({ count: ++this.state.count });
};
return `
<button data-action-click="handleClick">
Count: ${this.state.count}
</button>`;
},
});
ClickCounter.mount("#app");
const Parent = createComponent(
({ props }) => `
<div>
<slot></slot> <!-- Will auto-map to props.children.default -->
</div>
`
);
const Child = createComponent(() => `<p>Hello</p>`);
Parent.mount({ children: Child });
Create a shared state store with event-based updates:
import { shared } from "@magnumjs/micro-ui/utils";
const auth = shared("auth", { user: null });
auth.subscribe(console.log); // Logs current and future state
auth.emit("login", { user: "Tova" }); // auto-merges into state
You can on(event, fn)
to subscribe to specific events (e.g. "login"
, "logout"
).
auth.on("logout", () => console.log("logged out"));
auth.emit("logout", { user: null });
You can declaratively bind handlers in your template:
const Demo = createComponent(
() => `
<button data-action="sayHello" data-args='{"name":"Tova"}'>Hi</button>
`,
{
on: {
"click:sayHello"({ args }) {
alert(`Hello, ${args[0]}!`);
},
},
}
);
const MyCard = createComponent(
({ props: { title = "", children } }) => `
<div class="card">
<header data-slot="header">${title}</header>
<main><slot></slot></main>
<footer data-slot="footer">Default Footer</footer>
</div>
`
);
MyCard.mount("#demo");
MyCard.update({
children: {
default: "<p>Hello world!</p>",
footer: "<p>Custom footer here</p>",
},
});
Each component automatically has this.state
and this.setState
. Usage:
const Counter = createComponent(
function () {
const count = this.state.count ?? 0;
return `<button>Count: ${count}</button>`;
},
{
onMount() {
this.setState({ count: 0 });
},
on: {
"click button"(e) {
this.setState((s) => ({ count: s.count + 1 }));
},
},
}
);
Named slots work with both <slot name="x">
and <div data-slot="x">
.
const Card = createComponent(
() => `
<section>
<header data-slot="title">Default Title</header>
<main><slot></slot></main>
<footer data-slot="footer">Default Footer</footer>
</section>
`
);
createComponent(() => "<p>Lifecycle</p>", {
onBeforeMount() {
// Called before initial mount (async supported)
},
onMount() {
// Called after initial mount
},
onUpdate(prevProps) {
// Called after update render
},
onBeforeUnmount(next) {
// Delay unmount with callback or Promise or just sync
setTimeout(() => next(), 100);
},
onUnmount() {
// Final cleanup logic
},
});
π Core API Docs
// Object instance style: createComponent returns a component instance
const Comp = createComponent({
render({ state }) {
return state.show ? `
<div data-ref="container">
<span>${state.count}</span>
<button data-ref="inc">+</button>
</div>
` : null;
},
state: { count: 0, show: true },
on: {
"click [data-ref='inc']": ({ setState, state }) => {
setState({ count: state.count + 1 });
}
},
// Add lifecycle handlers directly on the instance
onMount() {
console.log("Mounted!");
},
onUpdate(prevProps) {
console.log("Updated!");
},
onUnmount() {
console.log("Unmounted!");
}
});
// Instance can be called in literals, with toString override:
const html = `<section>${Comp({ show: true })}</section>`;
// Inline actions with data-action-event:
const Demo = createComponent({
render() {
return `<button data-action-click="sayHello" data-name="${}">Say Hi</button>`;
},
sayHello() {
alert("Hello!");
}
});
// Hooks: effect, state, context ..
// Compose your own hooks, e.g. useFetch
import { useEffect, useState, useContext } from "@magnumjs/micro-ui/hooks";
function useFetch(url) {
const data = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(data.set);
}, [url]);
return data;
}
Comp.mount(target)
β Mount to target containerComp.update(nextProps)
β Update props and re-renderComp.setState(nextState)
β Trigger state updateComp.unmount()
β Cleanly unmount componentComp.renderFn()
β Returns the original component as String
Auto-populated with the Parent Node
after mount.
Auto-populated with [data-ref="name"]
nodes after mount.
Lazy accessor for a single ref. Returns null after unmount.
Auto-populated with props
from Comp.update(nextProps)
before each render.
Auto-populated with state
after setState.
If render()
returns null
, the previous DOM is cached and restored if render()
returns content again.
Renders keyed list efficiently:
import { renderList } from "@magnumjs/micro-ui/utils";
renderList(
data,
(item) => `<li>${item.label}</li>`,
(item) => item.id
);
Auto-wraps each root tag with data-key
for DOM diffing.
const Counter = createComponent(
({ state, setState }) => {
return `
<button data-ref="btn">${state.count}</button>
`;
},
{
state: { count: 0 },
on: {
"click [data-ref='btn']": ({ state, setState }) => {
setState({ count: state.count + 1 });
},
},
}
);
Mount and assert changes after click.
Pull requests are welcome!
Built with β€οΈ by developers who love simplicity.