EchoX β
EchoX is a lightweight reactive UI framework for declarative DOM manipulation, offering a simple and efficient alternative to React, Vue, and jQuery, especially for small projects.
It works out of the box without the need for compilation or transpilation while still providing the following benefits:
- Fine-grained interactivity
- Readable template
- Fully TypeScript support
The philosophy for EchoX is UI = f(DOM, Reactive), and APIs are designed based on this.
import {html, reactive} from "echox";
const [state] = reactive()
.state("value", 0)
.computed("double", (d) => d.value * 2)
.effect((d) => console.log(d.value, d.double))
.join();
const counter = html.div([
html.button({onclick: () => state.value++}, ["π"]),
html.button({onclick: () => state.value--}, ["π"]),
html.span([() => state.double]),
]);
document.body.appendChild(counter);Functional UI Construction β
EchoX provides a declarative way to building user interfaces with pure function calls, without compilation like JSX (used in React), and with full TypeScript support over string-based templates, portable and readable (used in Vue and Alpine).
A html proxy object exported to build nested UI.For example, let's create a counter:
html.div([
html.button({style: "background: blue"}, ["π"]),
html.button({style: "background: red"}, ["π"]),
html.span([0]),
]);Please refer to EchoX DOM for more information.
Native DOM Manipulation β
Operates directly on the native DOM instead of relying on a virtual DOM, achieving higher performance and lower memory overhead while maintaining simplicity.
The html proxy object create native DOM directly. This is the DOM in the philosophy. For example, to create a hello world message:
// The dom variable is a native DOM, not a virtual dom!!!
const dom = html.span({style: "font-size: 10"}, ["hello World"]);
// So you can directly append dom to the DOM tree!
container.appendChild(dom);Granular State Observation β
Apply fine-grained state observation, allowing independently update, minimizing unnecessary DOM updates and improves performance compared to virtual DOM-based frameworks. (Similar to SolidJS)
EchoX exports one method reactive for reactivity. For example, let's make the counter interactive:
const [scope] = reactive()
.state("value", 0)
.computed("double", (d) => d.value * 2)
.effect((d) => console.log(d.value, d.double))
.join();
const dom = html.div([
html.button({onclick: () => scope.value++}, ["π"]),
html.button({onclick: () => scope.value--}, ["π"]),
html.span([() => scope.double]),
]);EchoX.reactive returns a reactive scope that holds the states you defined. You bind them in DOM by using functions for attributes, styles, or children so reads track dependencies. This is the reactive in the philosophy.
Please refer to EchoX Reactive for more information.
Getting Started β
There are several way to using EchoX.
Installing from Package Manager β
EchoX is typically installed via a package manager such as Yarn or NPM.
$ npm add -S echox$ pnpm add -S echox$ yarn add -S echox$ bun add -S echoxEchoX can then imported as a namespace:
import {html} from "echox";
const dom = html.div(["hello world"]);
document.body.append(dom);Imported as an ES module β
In vanilla html, EchoX can be imported as an ES module, say from jsDelivr:
<script type="module">
import {html} from "https://cdn.jsdelivr.net/npm/echox/+esm";
const dom = html.div(["hello world"]);
document.body.append(dom);
</script>UMD Bundle β
EchoX is also available as a UMD bundle for legacy browsers.
<script src="https://cdn.jsdelivr.net/npm/echox"></script>
<script>
const dom = ex.html.div(["hello world"]);
document.body.append(dom);
</script>API Index β
- ex.html - create a html DOM with the specified attributes and child nodes.
- ex.reactive - create a reactive scope, where store the declared states.
- reactive.state - declare a state.
- reactive.computed - derive a computed state.
- reactive.effect - observe a effect.
- reactive.join - get the states from the reactive scope.
html.[tagName]([attributes,] children) β
html is a proxy: each property is a tag factory (for example html.div, html.span). Calling html(tagNamespace) returns another proxy that creates elements in that namespaceβuseful for SVG:
const svg = html("http://www.w3.org/2000/svg");
svg.circle({cx: 50, cy: 50, r: 40});For a given tag, the signature is tagName(attributes?, children?). If the first argument is a plain object, it is treated as attributes and the second argument is children; otherwise the first argument is children and attributes default to {}.
Children are flattened (nested arrays are supported). Values that are not mountable are skipped (
null,undefined,false);0is kept and rendered as text. Primitives become text nodes; existing DOM nodes are inserted as-is. A function child is reactive: it is re-run when tracked state changes, and its return value (nodes, text, arrays, ornull) replaces that fragment.Attributes map to DOM properties or attributes. Values may be static or functions that read reactive state; those update when dependencies change. A
styleobject sets inline styles; each style field may also be a function. Keys starting withonare registered as event listeners (onclick, β¦).
html.p(["Hello", html.em(["EchoX"])]);reactive() β
Returns a builder (Reactive) used to declare state and effects. Chain .state, .computed, and .effect, then call .join() to materialize a live scope object. Definition order between state and computed does not matter.
const rx = reactive();
rx.state("n", 0).computed("double", (s) => s.n * 2);reactive.state(key, value) β
Registers a state named key with initial value. After join(), the scope exposes key as a readable and assignable property; assignments schedule updates to any computed, html bindings, and effects that depend on that state.
reactive().state("count", 0).state("label", "go");reactive.computed(key, define) β
Registers a derived state under key. define is a function (scope) => value that reads other fields on scope. Computed values are recomputed when their dependencies change (lazily on read, with updates batched so a dependency change does not run the same computed more than once per flush).
reactive()
.state("x", 2)
.computed("area", (s) => s.x * s.x);reactive.effect(effect) β
Registers a side effect. Effects run after join(), and again when any state read inside the effect changes. If effect(scope) returns a function, that function is treated as a cleanup and is called when the dispose function from join() runs (see below). Non-function return values are ignored.
reactive()
.state("n", 0)
.effect((s) => {
console.log("n =", s.n);
return () => console.log("cleanup");
});reactive.join() β
Builds the reactive scope from the declared state and computed entries, runs all effects once, and returns a tuple [scope, dispose].
scope: proxy whose properties correspond to state and computed keys.dispose: call with no arguments to run every cleanup function returned from an effect (useful for tearing down subscriptions or manual DOM work).
const [scope, dispose] = reactive().state("n", 0).effect((s) => s.n).join();
scope.n = 1;
dispose();