The Landscape
How the big frameworks solve the same problems
Every UI framework solves three problems: reactivity, binding, and reconciliation. They just solve them differently.
Understanding these differences IS understanding modern web dev.
| Framework | Reactivity | Binding | Reconciliation |
|---|---|---|---|
| React | useState + scheduler |
JSX compilation | Fiber + Virtual DOM |
| Vue 3 | Proxy-based ref/reactive |
Template compiler | Virtual DOM diff |
| Svelte | Compile-time analysis | Compile-time | Surgical DOM ops |
| Solid | Fine-grained signals | JSX + reactivity | No VDOM, direct updates |
| Alpine | Proxy (like qrazy) | Runtime attributes | Simple re-render |
| htmx | Server is the state | HTML responses | Replace HTML chunks |
| qrazy | Proxy | Runtime attributes | Keyed Map |
The same counter, six ways
Let's implement a simple counter in each framework. Same functionality, different philosophies.
React
"UI as a function of state"import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</button>
);
}
How it works: useState returns a value and a setter. Calling the setter schedules a re-render. React re-runs the entire function, builds a virtual DOM tree, diffs it against the previous tree, and patches the real DOM.
Vue 3
"Progressive framework"<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++">
Clicked times
</button>
</template>
How it works: ref() wraps a value in a Proxy. The template compiler tracks which parts of the template depend on which refs. When a ref changes, Vue re-renders only the affected parts via virtual DOM diffing.
.value to access refs in JS. Two APIs (Options vs Composition). Virtual DOM still has overhead.
Svelte
"Compiler does the work"<script>
let count = 0;
</script>
<button on:click="{() => count++}">
Clicked {count} times
</button>
How it works: The Svelte compiler analyzes your code at build time. It knows exactly which DOM nodes depend on count. The output is vanilla JS that directly updates those specific text nodes. No runtime framework, no virtual DOM.
Solid
"True fine-grained reactivity"import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicked {count()} times
</button>
);
}
How it works: Signals are reactive primitives. When you read count() inside JSX, Solid tracks that dependency. When you call setCount(), only the exact text node that reads count() updates. No re-render, no diffing.
count(). Mental model shift from React. Smaller ecosystem.
Alpine
"jQuery for the modern web"<div x-data="{ count: 0 }">
<button @click="count++">
Clicked <span x-text="count"></span> times
</button>
</div>
How it works: Alpine scans the DOM at runtime for x-* attributes. It wraps the data in a Proxy and re-evaluates bindings when state changes. Very similar to qrazy—we basically built a mini-Alpine.
htmx
"Hypermedia as the engine"<!-- Client -->
<button hx-post="/increment"
hx-swap="innerHTML">
Clicked 0 times
</button>
# Server (returns HTML, not JSON)
POST /increment
return "Clicked 1 times"
How it works: htmx intercepts user actions and makes HTTP requests. The server responds with HTML, not JSON. htmx swaps that HTML into the page. The server IS the state—no client-side state management needed.
qrazy
"Learn by building"<div q-data="{ count: 0 }">
<button q-click="count++">
Clicked <span q-text="count"></span> times
</button>
</div>
How it works: Almost identical to Alpine. Proxy-based reactivity, runtime attribute parsing, stored updater functions. 250 lines of code you can read in an afternoon.