I Built a Tiny Reactive JS Library — and Rediscovered Why v-model Exists
When you use frameworks like Vue or React, a lot of things “just work”.
Recently, I tried to build a tiny reactive JavaScript library called Signel.js, and I learned why those things exist — the hard way.
Why I built it
I wanted to understand reactivity better, so I built a small library using:
-
Proxyfor state - simple template interpolation (
$$value) - manual DOM bindings
No virtual DOM. No JSX. Just raw JavaScript.
The demo: currency converter
I built a small USD → UZS currency converter using a free exchange-rate API.
let state = el(".exchange", {
usdAmount: "",
uzsAmount: ""
});
I bound the inputs like this:
input("#usd", state, "usdAmount");
input("#uzs", state, "uzsAmount");
And watched the USD value:
watch(state, "usdAmount", async value => {
const res = await fetch(
"https://hexarate.paikama.co/api/rates/USD/UZS/latest"
);
const data = await res.json();
state.uzsAmount = (value * data.data.mid).toFixed(2);
});
The bug that confused me
Here’s what happened:
-
$$uzsAmountin the template updated correctly ✅ - but the
<input id="uzs">did not update ❌
At first, I thought:
- API issue?
- async bug?
- state not reactive?
But the state was changing.
The real problem
My el() function re-rendered the element like this:
el.innerHTML = template.replace(...)
That means on every state change:
- inputs were destroyed
- new inputs were created
- old event listeners and bindings were lost
So input() was bound to a DOM element that no longer existed.
This explains why:
- text interpolation worked
- inputs silently failed
The fix: model() instead of input()
I already had a model() helper inspired by v-model.
model("#usd", state, "usdAmount");
model("#uzs", state, "uzsAmount");
model() stores state → DOM bindings as functions, so even after re-renders, the input value is updated correctly.
After switching to model():
- ✅ USD input works
- ✅ UZS input updates
- ✅ Text interpolation still works
What I learned
This bug taught me more than any tutorial:
- Why frameworks avoid full
innerHTMLre-renders - Why form bindings are special
- Why v-model-style abstractions exist
- Why DOM diffing and controlled inputs matter
I accidentally rediscovered a problem that led to React, Vue, and Svelte.
Final thoughts
Building tiny libraries is an amazing way to understand big frameworks.
If you want to learn how reactivity really works:
- build something small
- break it
- fix it
That’s how frameworks are born.
🔗 Signel.js (MIT, experimental)
Online demo
Top comments (0)