A pull-based reactive system with lazy evaluation, automatic memory management, and declarative DOM bindings
npm install latchjsA pull-based reactive system with automatic memory management



---
- Pull-Based Reactivity: Values recompute only when read, not when dependencies change
- Automatic Memory Management: Uses WeakRef + FinalizationRegistry for cleanup without manual dispose
- AsyncIterator Streams: Consume reactive changes with for await...of and natural backpressure
- No Build Step Required: Works directly in browsers via ES modules or IIFE
- Declarative DOM Bindings: @click, :class, @for, @if directives work at runtime
- Component System: Props, slots, events, scoped styles, and lifecycle hooks
---
``bash`
npm install latchjs
Or use via CDN:
`html`
Or ES modules:
`html`
---
` Count: {{ count }} html
Hello {{ name }}!
10">That's a lot!
0">Keep going...
Click to start
`
`js
import { reactive, computed, effect, bind } from 'latchjs';
const state = reactive({ price: 10, quantity: 2 });
const total = computed(() => state.price * state.quantity);
effect(() => {
console.log(Total: $${total.value});
});
bind(document.getElementById('output'), {
text: () => $${total.value},
class: { expensive: () => total.value > 50 },
});
state.quantity = 5; // Effect logs: "Total: $50"
`
`js
import { ReactiveElement } from 'latchjs';
class ProductCard extends ReactiveElement {
static reactive = { quantity: 1 };
static template =
;
static bindings = {
'.qty': { text: s => s.quantity },
'.add': { on: { click: s => s.quantity++ } }
};
}customElements.define('product-card', ProductCard);
`$3
`js
import { defineComponent, mountAllComponents } from 'latchjs';defineComponent({
name: 'user-card',
props: {
name: { type: 'string', required: true },
role: { type: 'string', default: 'Member' }
},
template:
,
styles: .card { padding: 1rem; border: 1px solid #ccc; },
setup(props) {
return { name: props.name, role: props.role };
}
});// Content
mountAllComponents(document.body);
`---
API Reference
$3
####
reactive(object)Creates a deeply reactive proxy.
`js
const state = reactive({ user: { name: 'Alice' }, items: [] });
state.user.name = 'Bob';
state.items.push({ id: 1 });
`####
computed(fn)Lazy computed value. Only recomputes when read after dependencies change.
`js
const total = computed(() => state.items.reduce((s, i) => s + i.price, 0));
console.log(total.value);
`####
effect(fn)Runs side effects when dependencies change. Returns stop function.
`js
const stop = effect((onCleanup) => {
const timer = setInterval(() => console.log(state.count), 1000);
onCleanup(() => clearInterval(timer));
});
stop();
`####
watch(source, callback, options?)Watch specific values with old/new comparison.
`js
watch(
() => state.user.name,
(newVal, oldVal) => console.log(${oldVal} → ${newVal}),
{ immediate: true }
);
`####
iterate(source)Consume changes as AsyncIterator with backpressure.
`js
for await (const value of iterate(() => state.query)) {
await processValue(value);
}
`####
ref(value)Simple reactive reference.
`js
const count = ref(0);
count.value++;
`####
readonly(object) / shallowReactive(object)`js
const readonlyState = readonly(state);
const shallow = shallowReactive({ a: { b: 1 } });
`$3
####
reactiveMap() / reactiveSet()`js
const map = reactiveMap(new Map([['a', 1]]));
const set = reactiveSet(new Set([1, 2, 3]));effect(() => console.log('Map size:', map.size));
map.set('b', 2);
`$3
####
batch(fn) / flushSync()`js
batch(() => {
state.a = 1;
state.b = 2;
});
flushSync();
`$3
#### Directives
| Directive | Shorthand | Description |
|-----------|-----------|-------------|
|
{{ expr }} | | Text interpolation |
| r-text="expr" | | Set text content |
| r-html="expr" | | Set innerHTML (sanitized) |
| r-show="expr" | | Toggle visibility |
| r-if="expr" | @if | Conditional render |
| r-else-if="expr" | @else-if | Chained condition |
| r-else | @else | Else branch |
| r-for="item in items" | @for | List rendering |
| r-model="prop" | | Two-way binding |
| r-class:name="expr" | .name | Toggle class |
| r-style:prop="expr" | | Set style property |
| r-attr:name="expr" | :name | Set attribute |
| r-on:event="handler" | @event | Event listener |
| r-transition="name" | | CSS transitions |#### Event Modifiers
`html
`Modifiers:
.prevent, .stop, .once, .capture, .passive, .enter, .esc, .tab, .space, .up, .down, .left, .right, .delete#### Transitions
`html
Content
`####
bind(element, config)`js
bind(element, {
text: () => state.label,
html: () => state.richContent,
show: () => !state.hidden,
class: { active: () => state.isActive },
style: { opacity: () => state.isVisible ? '1' : '0' },
attr: { disabled: () => state.loading ? '' : null },
on: { click: () => state.count++ }
});
`####
mount(selector, state)`js
mount('#app', reactive({ count: 0 }));
`$3
####
defineComponent(options)`js
defineComponent({
name: 'my-button',
props: {
label: { type: 'string', required: true },
variant: { type: 'string', default: 'primary' }
},
emits: ['click'],
template: ,
styles: .primary { background: blue; },
setup(props, { emit }) {
return {
label: props.label,
variant: props.variant,
handleClick: () => emit('click')
};
}
});
`| Function | Description |
|----------|-------------|
|
defineComponent(options) | Register component |
| createComponent(name, props?) | Create instance |
| mountComponent(el, name, props?) | Mount to element |
| mountAllComponents(root?) | Auto-mount all |
| getComponent(name) | Get definition |
| hasComponent(name) | Check exists |
| listComponents() | List all |
| unregisterComponent(name) | Remove |#### Built-in Components
`html
...
...
`$3
`js
import { sanitizeHTML, configureSanitizer } from 'latchjs';const safe = sanitizeHTML('
Hello
');
// 'Hello
'configureSanitizer({
allowedTags: ['p', 'a', 'strong'],
allowDataUrls: false
});
`$3
`js
import { configureWarnings, showDebugPanel, printSummary } from 'latchjs';configureWarnings({ level: 'warn', throwOnError: false });
showDebugPanel();
printSummary();
`$3
| Function | Description |
|----------|-------------|
|
toRaw(proxy) | Get original object |
| isReactive(value) | Check if reactive |
| isComputed(value) | Check if computed |
| isRef(value) | Check if ref |
| isReadonly(value) | Check if readonly |
| cleanupElement(el) | Manual cleanup |---
Examples
| Example | Description |
|---------|-------------|
| counter.html | Counter implementations |
| todo.html | Task manager |
| search.html | Real-time search |
| dashboard.html | Analytics dashboard |
| form.html | Form validation |
| ecommerce.html | Shopping cart |
| async-stream.html | AsyncIterator demo |
| components.html | Component system |
| devtools.html | DevTools panel |
`bash
npx serve examples
`---
TypeScript
`ts
import { reactive, computed, ref, Ref, ComputedRef } from 'latchjs';interface State {
user: { name: string; age: number };
items: string[];
}
const state = reactive({
user: { name: 'Alice', age: 30 },
items: []
});
const count: Ref = ref(0);
const doubled: ComputedRef = computed(() => count.value * 2);
`---
Browser Support
Requires:
Proxy, WeakRef, FinalizationRegistry, queueMicrotaskChrome 84+, Firefox 79+, Safari 14.1+, Edge 84+
---
Development
`bash
npm install
npm run build
npm test # 354 tests
npm run dev
npm run typecheck
``---
MIT