Thinking in Elur
Elur's mental model is different from React's. In React, you think in terms of "re-renders": when state changes, the component function runs again and the virtual DOM is diffed. In Elur, components run once and the framework tracks which pieces of the DOM depend on which signals. When a signal changes, only the dependent DOM nodes are updated.
# The signal graph
At the heart of Elur is a dependency graph of signals, computed values, and effects:
signal(0) ──┬── computed(() => count * 2)
├── effect(() => console.log(count))
└── DOM text node "Count: ${() => count}"When you write count.value = 5, Elur walks the graph and notifies every
dependent: the computed recalculates, the effect re-runs, and the text node
updates its content. Nothing else is touched.
# Rules of reactivity
Read inside a tracking context. A signal is only tracked when read inside a reactive function — a computed, an effect, or a template interpolation (
${() => ...}).Write outside of tracking. You should not write to a signal from inside a computed (it would create a loop). Write from event handlers, effects with guards, or external code.
Functions are reactive.
${() => count.value}is called once during the initial render (setting up the dependency tracking), and again only whencountchanges.
# What runs when?
| Action | What runs |
|---|---|
| Initial render | All interpolation functions, all effect (eager). computed only if its .value is read. |
signal.value = x |
Only dependents: computed (eager after first read), effect, DOM nodes |
| Component re-render | Never — components run once |
This is why Elur is fast: there is no re-render. The component function executes exactly once, setting up the reactive graph. After that, all updates are surgical.
# Comparing to React
// React: the entire component re-runs on every state change
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>{count}</button>;
}// Elur: the component runs once; only the text node updates
const Counter = defineComponent(() => {
const count = signal(0);
return html`<button @click=${() => count.value++}>${() => count.value}</button>`;
});In React, clicking the button calls setCount, which re-runs Counter,
re-creates the JSX, and diffs it against the previous tree. In Elur, clicking
the button increments the signal, and Elur directly updates the text node's
textContent. No diffing, no re-execution.
# Comparing to Vue
Vue's ref() and computed() are conceptually similar to Elur's signal()
and computed(). The key difference is the templating layer: Vue uses a
compiled template DSL, while Elur uses JavaScript tagged template literals.
This means Elur templates are just JavaScript — no compiler, no special syntax
to learn beyond the html tag.
# When to use what
- Use
signal()for independent reactive values. - Use
computed()for values derived from other signals. - Use
effect()for side effects that should re-run when dependencies change (e.g., syncing tolocalStorage). - Use
html\` for declarative DOM rendering. - Use
repeat()for keyed list rendering.
If you're coming from React, the biggest shift is: don't think about re-renders. Think about which signals your UI depends on, and let the reactivity system handle the rest.