This content originally appeared on DEV Community and was authored by Fedor
Today’s Challenge: Build a 1KB Frontend Library
Let’s tackle an exciting challenge today: creating a frontend library that’s just 1 kilobyte in size. I’m talking about a "disappearing framework" — not like Svelte, which only "disappears" after compilation. No build tools, no bloated node_modules folder hogging your SSD. Just a few lightweight JavaScript functions you can copy, paste, and use right away.
Buckle up!
Reactivity with Signals
By 2025, the frontend world has largely agreed on one thing: signals for reactivity. Almost every major framework has its own version of signals — like Vue’s ref() or Svelte’s $state rune.
If you’re new to signals, don’t worry. Just remember two key concepts:
- Signals: Reactive values that can be read and updated.
- Effects: Functions that depend on signals. When a signal changes, its dependent effects automatically re-run.
A Tiny Signals Implementation
Our compact signals implementation is inspired by Andrea Giammarchi’s excellent article on signals. If you’re curious about the nitty-gritty, I highly recommend giving it a read.
{
const effects = [Function.prototype];
const disposed = new WeakSet();
function signal(value) {
const subs = new Set();
return (newVal) => {
if (newVal === undefined) {
subs.add(effects.at(-1));
return value;
}
if (newVal !== value) {
value = newVal?.call ? newVal(value) : newVal;
for (let eff of subs) disposed.has(eff) ? subs.delete(eff) : eff();
}
};
}
function effect(fn) {
effects.push(fn);
try {
fn();
return () => disposed.add(fn);
} finally {
effects.pop();
}
}
}
function computed(fn) {
const s = signal();
s.dispose = effect(() => s(fn()));
return s;
}
How It Works:
- We use a block scope (
{}) to keep our variables out of the global namespace. This is handy when modules aren’t an option. - The
signalfunction creates a reactive value. It returns a function that acts as both a getter and setter:- If called without arguments, it returns the current value and subscribes the active effect to the signal.
- If called with a new value, it updates the signal and triggers all subscribed effects (unless they’re disposed).
- The
effectfunction registers a callback that runs immediately and re-runs whenever any of its dependent signals change. - The
computedfunction creates a derived signal — a reactive value that is recalculated every time it's dependencies change.
Example Usage:
const count = signal(0); // Create a signal with initial value 0
effect(() => {
console.log(`Count is: ${count()}`); // Log the current value of the signal
});
count(1); // Update the signal, which triggers the effect and logs "Count is: 1"
count(2); // Update again, logs "Count is: 2"
Reactive HTML Templates
Now, let’s add some templating and rendering magic. We’ll create a tagged template function, html, that parses HTML strings and dynamically binds reactive values to the DOM.
{
function html(tpl, ...data) {
const marker = "\ufeff";
const t = document.createElement("template");
t.innerHTML = tpl.join(marker);
if (tpl.length > 1) {
const iter = document.createNodeIterator(t.content, 1 | 4);
let n,
idx = 0;
while ((n = iter.nextNode())) {
if (n.attributes) {
if (n.attributes.length)
for (let attr of [...n.attributes])
if (attr.value == marker) render(n, attr.name, data[idx++]);
} else {
if (n.nodeValue.includes(marker)) {
let tmp = document.createElement("template");
tmp.innerHTML = n.nodeValue.replaceAll(marker, "<!>");
for (let child of tmp.content.childNodes)
if (child.nodeType == 8) render(child, null, data[idx++]);
n.replaceWith(tmp.content);
}
}
}
}
return [...t.content.childNodes];
}
const render = (node, attr, value) => {
const run = value?.call
? (fn) => {
let dispose;
dispose = effect(() =>
dispose && !node.isConnected ? dispose() : fn(value())
);
}
: (fn) => fn(value);
if (attr) {
node.removeAttribute(attr);
if (attr.startsWith("on")) node[attr] = value;
else
run((val) => {
if (attr == "value" || attr == "checked") node[attr] = val;
else
val === false
? node.removeAttribute(attr)
: node.setAttribute(attr, val);
});
} else {
const key = Symbol();
run((val) => {
const upd = Array.isArray(val)
? val.flat()
: val !== undefined
? [document.createTextNode(val)]
: [];
for (let n of upd) n[key] = true;
let a = node,
b;
while ((a = a.nextSibling) && a[key]) {
b = upd.shift();
if (a !== b) {
if (b) a.replaceWith(b);
else {
b = a.previousSibling;
a.remove();
}
a = b;
}
}
if (upd.length) (b || node).after(...upd);
});
}
}
}
Key Features:
- The
htmlfunction returns an array of DOM nodes. - It supports dynamic attributes, text content, child nodes, and event listeners using the
on*syntax. - If the provided value is a function (or a signal itself) it sets up an effect which is re-run to update the DOM.
Example Usage:
// Reactive state
const count = signal(0);
// Render the app
const app = html`<div>
<h1>Counter: ${count}</h1>
<button onclick=${() => count((val) => val + 1)}>Increment</button>
<button onclick=${() => count((val) => val - 1)}>Decrement</button>
</div>`;
// Mount the app to the DOM
document.body.append(...app);
A More Complex Example: A Todo App
Check out this interactive Todo app built with our tiny library. It’s a great example of what you can achieve with just a few lines of code.
What’s Next?
In the next installment, we’ll add efficient list re-rendering with just one function. Stay tuned! 🚀
This content originally appeared on DEV Community and was authored by Fedor
Fedor | Sciencx (2025-02-08T23:19:29+00:00) 1KB Frontend Library. Retrieved from https://www.scien.cx/2025/02/08/1kb-frontend-library/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.