nhα»β "small" in Vietnamese
A tiny (~1.2KB gzipped) library for building Web Components.
- Small β
1294 bytes(esm),1578 bytes(umd), gzipped - Familiar β Vue-inspired API
- Simple β reactive state, lifecycle hooks, effects, refs
- Tested β
100%coverage
Live demo: Ecommerce Cart Β· source
Vanilla Web Components are tedious, full frameworks are overkill β Nho is the happy middle: tiny, reactive, no fuss.
Nho is published to GitHub Packages under the @anh-ld scope. Point the scope at the GitHub registry in your .npmrc:
@anh-ld:registry=https://npm.pkg.github.com
Then install:
npm install @anh-ld/nhoimport { Nho } from "@anh-ld/nho";
class MyCounter extends Nho {}/* Global styles β injected into every Nho element */
Nho.style = `
.box { background: blue; color: yellow; }
`;
class MyCounterChild extends Nho {
render(h) {
return h`<div>Child: ${this.props.count}</div>`;
}
}
class MyCounter extends Nho {
setup() {
/* runs before mount */
this.state = this.reactive({ count: 1 }); /* state must be an object */
this.pRef = this.ref(); /* holds a DOM reference */
/* run callback whenever the value changes */
this.effect(
() => this.state.count,
(oldValue, newValue) => console.log(oldValue, newValue),
);
}
/* lifecycle hooks β all optional */
onMounted() {
console.log("mounted");
}
onUpdated() {
console.log("updated, p ref:", this.pRef.current);
}
onUnmounted() {
console.log("before unmount");
}
addCount() {
/* reassign a key β don't replace the whole state object */
this.state.count += 1;
}
render(h) {
/* one root element; bind state/events inline; pass props with "p:" */
return h`
<div class="box">
<p ref=${this.pRef}>Count: ${this.state.count}</p>
<button onclick=${this.addCount}>Add</button>
<my-counter-child p:count=${this.state.count + 5}></my-counter-child>
</div>
`;
}
}
customElements.define("my-counter", MyCounter);
customElements.define("my-counter-child", MyCounterChild);<my-counter></my-counter>| Member | Description |
|---|---|
setup() |
Runs before mount. Initialize state, refs, effects here. |
render(h) |
Returns the template. h is a tagged-template helper. |
reactive(obj) |
Returns reactive state; mutating a key triggers a batched re-render. |
ref(initial?) |
Returns { current }, for DOM references via ref=. |
effect(valueFn, cb) |
Runs cb(old, new) when valueFn() changes between updates. |
onMounted() / onUpdated() / onUnmounted() |
Lifecycle hooks. |
Nho.style |
Static string of CSS injected into every element. |
Reserved β don't define your own members named above,
props, or anything starting with_.
flowchart LR
A[State change] --> B[Batched re-render] --> C[Render & diff] --> D[Patch DOM] --> E[Bind + lifecycle]
Diffing is index-based: trim extra children, compare each child by index, clone if missing, replace on tag/text mismatch, and sync attributes in place. Props, on* handlers, and refs are cached during render and re-attached after patching.
- No
key,Fragments, ormemoβ basic diffing, best for small/medium components. - No keyed reordering β list diffing is index-based, so reordered items reuse existing nodes and identity can drift.
- No escaping in text interpolation β interpolated values insert raw HTML. Don't pass untrusted input into templates.
p:props needhβ stringp:props on custom elements created outsidehresolve toundefined; create them insideh.
For complex UIs, use a full framework.
bun install # dependencies
bun run build # build library bundles
bun run dev # build/watch the example (open example/index.html)
bun run serve # serve the example folder
bun test # run tests