literaljs
v8.1.8
Published
A small JavaScript library for building reactive user interfaces.
Readme
Quick Start
Install
npm install literaljsHello World
/** @jsx h */
import { h, component, App } from 'literaljs';
const Counter = component({
name: 'Counter',
state: { count: 0 },
methods() {
return {
increment() {
this.setState({ count: this.getState().count + 1 });
}
};
},
render() {
return (
<button events={{ click: () => this.increment() }}>
Count: {this.getState().count}
</button>
);
}
});
new App(Counter, {}).mount('app');JSX setup: Configure your build tool to use
has the pragma:{ "plugins": [["@babel/plugin-transform-react-jsx", { "pragma": "h" }]] }
CDN (UMD)
<script src="https://unpkg.com/literaljs@8/build/index.umd.js"></script>
<script>
const { h, component, App } = LiteralJS;
// same as above
</script>Public API
| Export | Purpose |
|--------|---------|
| h(tag, attrs, ...children) | Create a VNode (use as JSX pragma, Hyperscript, or plain object) |
| component(config) | Define a component with state, methods, and lifecycle |
| new App(rootComponent, initialStore) | Create an isolated app instance |
| app.mount(domId) | Mount to a DOM element by ID |
| app.unmount() | Remove from DOM, clean up events and state |
| app.update() | Force a re-render |
| app.setStore(updater) | Update the global store |
| app.getStore() | Read the global store |
| render(rootComponent, domId, store) | Legacy one-liner (backward-compatible) |
| StateManager | Standalone state manager (for testing) |
| generateUUID | UUID generator |
| advanceRenderCycle | Advance the render cycle counter (testing) |
Component API
const MyComponent = component({
name: 'MyComponent', // Component name (for debugging and state keys)
state: { count: 0 }, // Initial component state
methods() { // Bound methods available as this.methodName
return {
increment() {
this.setState({ count: this.getState().count + 1 });
}
};
},
mounted() { // Called after first render
console.log('Mounted!');
},
updated() { // Called after re-render
console.log('Updated!');
},
unmounted() { // Called when removed from DOM
console.log('Cleaned up!');
},
render() { // Returns a VNode tree
const { count } = this.getState();
return (
<div>
<p>Count: {count}</p>
<button events={{ click: () => this.increment() }}>
Increment
</button>
</div>
);
}
});Props
Pass data down from parent to child:
// Parent
<MyComponent key="unique" title="Hello" count={5} />
// Child
render() {
return <h1>{this.props.title} — {this.props.count}</h1>;
}Sibling Components (Auto-Keyed)
You don't need explicit key props for sibling components. LiteralJS automatically assigns stable, position-based instance keys so each sibling gets isolated state:
const Counter = component({
name: 'Counter',
state: { count: 0 },
methods() {
return {
increment() { this.setState({ count: this.getState().count + 1 }); }
};
},
render() {
return <span>{this.getState().count}</span>;
}
});
const App = component({
name: 'App',
render() {
// Each Counter has its own independent state — no key prop needed
return (
<div>
<Counter /> {/* auto-key: __auto_0__ */}
<Counter /> {/* auto-key: __auto_1__ */}
<Counter /> {/* auto-key: __auto_2__ */}
</div>
);
}
});How it works: Each render cycle, component factories reset a position counter. The first keyless sibling gets
__auto_0__, the second__auto_1__, etc. Because the counter resets each render, the same position always maps to the same key — so state persists across re-renders while remaining isolated between siblings.
You can still use explicit key props when you need semantic identity (e.g., for list items that reorder):
{items.map(item => (
<ListItem key={item.id} item={item} />
))}Lifecycle Hooks
| Hook | Called When | Use Case |
|------|------------|----------|
| mounted() | After first DOM render | Initialize third-party libs, DOM queries |
| updated() | After re-render | React to prop changes, DOM measurements |
| unmounted() | Before DOM removal | Clean up timers, event listeners, subscriptions |
Component Instance Methods
| Method | Description |
|--------|-------------|
| this.getState() | Shallow copy of component state |
| this.setState(updater) | Update state and trigger re-render |
| this.getStore() | Shallow copy of global store |
| this.setStore(updater) | Update global store and trigger re-render |
| this.props | Current component props |
State Management
Component State (private, isolated)
Each component instance has its own private state. Sibling instances do not share state:
this.getState(); // Read current state (shallow copy)
this.setState({ count: 5 }); // Update and re-renderGlobal Store (shared across components)
// Write
this.setStore({ theme: 'dark' });
// Read
const theme = this.getStore().theme;Initialize the store when creating the app:
new App(RootComponent, { theme: 'light', user: null }).mount('app');Keys and Reconciliation
Auto-Keys (v8.0.4+)
LiteralJS automatically assigns position-based keys to sibling components. You don't need to add key props unless you need semantic identity for reordering.
Explicit Keys
Use explicit key props when:
- List items can be reordered (keyed reconciliation tracks DOM identity)
- You want to force a component to remount when data changes
{items.map(item => (
<TodoItem key={item.id} item={item} />
))}Architecture
Design Principles
- Zero runtime dependencies — only dev dependencies for builds and tests
- Small surface area — 9 exports, one component pattern, one state model
- Plain functions — no classes, no hooks, no decorators, no magic
- Isolated instances — multiple
Appinstances on one page, zero globals
Module Structure
App (container)
├── StateManager — global store + component state with subscriptions
├── EventManager — WeakMap delegation + event cache
├── Renderer — DOM creation + virtual DOM diff/patch
├── LifecycleQueue — mounted/updated/unmounted lifecycle hooks
├── DiffEngine — keyed + non-keyed child reconciliation
├── MountOrchestrator — render cycle coordination + tree generation
├── UpdateLoop — microtask batched scheduler
├── ComponentInstance — per-instance state, methods, and lifecycle
└── VNode — lightweight virtual DOM representationv8 Performance Features
| Feature | Benefit |
|---------|---------|
| Keyed reconciliation | Preserves DOM identity across reordering |
| WeakMap event delegation | Skips ancestor walk when no handlers exist |
| Component memoization | Shallow props comparison skips re-render |
| Text node fast path | textContent update instead of replaceChild |
| Reference-equality shortcuts | Skips diffing for unchanged VNodes, children, attrs |
| Auto-instance keys | Sibling components isolated without manual key props |
| Microtask batching | Multiple setState calls collapse to one render |
Bundle Size
| Format | Minified | Gzipped | |--------|----------|---------| | ESM (modern) | 16.6 kB | 5.3 kB | | CJS | 16.6 kB | 5.3 kB | | UMD | 16.8 kB | 5.4 kB |
Target: Chrome 90+, Firefox 90+, Safari 14+ (ES2018). No Object.assign polyfill needed.
Performance
Benchmarks measured in JSDOM. Real Chrome results are significantly faster.
| Benchmark | v7 | v8.0.5 | v8.1.0 | Change (v7→v8.1) | |-----------|----|--------|--------|------------------| | create 1k rows | ~112ms | ~47ms | ~46ms | -59% | | create 10k rows | ~890ms | ~495ms | ~458ms | -49% | | replace 1k rows | ~65ms | ~10ms | ~0.3ms | -99.5% | | partial update | ~186ms | ~126ms | ~3.7ms | -98% | | select row | ~22ms | ~0.03ms | ~0.03ms | -99.8% | | swap rows | — | ~90ms | ~0.3ms | -99.7% | | remove row | ~74ms | ~8.6ms | ~0.2ms | -99.7% | | append rows | — | ~197ms | ~0.3ms | -99.8% | | clear rows | ~3ms | ~0.01ms | ~0.2ms | -93% |
Event Handling
LiteralJS uses event delegation. Define handlers via the events attribute:
<button events={{ click: () => this.increment() }}>
Click me
</button>Supported events: click, input, change, submit, keyup, keydown, focus, blur, mouseenter, mouseleave, touchstart, touchend, and more.
Syntax Options
JSX (recommended)
/** @jsx h */
import { h, component, App } from 'literaljs';
const View = component({
name: 'View',
render() {
return <div class="app"><h1>Hello</h1></div>;
}
});Hyperscript
const View = component({
name: 'View',
render() {
return h('div', { class: 'app' }, h('h1', {}, 'Hello'));
}
});Object syntax
const View = component({
name: 'View',
render() {
return { e: 'div', a: { class: 'app' }, c: [{ e: 'h1', a: {}, c: ['Hello'] }] };
}
});Style Handling
Pass styles as objects (not strings). String styles cause a TypeError because LiteralJS iterates over style keys:
// ✅ Correct
<div style={{ position: 'fixed', top: '0', left: '0' }}>
// ❌ Wrong — causes TypeError
<div style="position:fixed;top:0;left:0">Changelog
[8.1.8] - 2026-06-09
Fixed: State updates now correctly trigger DOM updates when component instances are returned directly from render().
- Fixed memoization in
tree.jsto preserve component instances instead of flattening them into VNodes - Added
cloneVNode()helper to properly clone VNode trees while preserving component instances - Component instances cached during memoization are now processed during tree recursion
- All 475 tests pass, including new regression tests for nested App and router-mounted App patterns
[8.0.4] - 2026-05-19
Fixed: Sibling components without explicit key props now have isolated state.
- Replaced
__default__singleton key with auto-incrementing position keys (__auto_0__,__auto_1__, etc.) - Added render-cycle counter (
advanceRenderCycle) for stable keys across re-renders MountOrchestratorcallsadvanceRenderCycle()before each render pass- Exported
advanceRenderCyclefor testing - Updated tree.js (simplified, removed
_resetPositionfrom VNode walk)
[8.0.3] - 2026-05-18
Fixed: Null/undefined guard in generateTree before typeof check.
[8.0.2] - 2026-05-18
Fixed: Tree direct-props fix — two code paths for backward compat (newC.a.props) and modern JSX ({ ...newC.a }).
[8.0.1] - 2026-05-18
Fixed: WeakMap event delegation. Component render memoization. Keyed reconciliation.
[8.0.0] - 2026-05-18
Initial v8 release. Full architecture refactor from monolith to 12 modules.
