
Hacker News · Mar 1, 2026 · Collected from RSS
Vertex is a 1kloc SPA framework containing everything you need from React, Ractive-Load and jQuery while still being jQuery-compatible. vertex.js is a single, self-contained file with no build step and no dependencies. Also exhibits the curious quality of being faster than over a decade of engineering at Facebook in some cases: https://files.catbox.moe/sqei0d.png Comments URL: https://news.ycombinator.com/item?id=47205659 Points: 7 # Comments: 1
Contents Obtaining vertex.js Loading in the <head> Setting a templates directory DOM layer — V$ / VQuery AJAX Fiber reconciler — createElement & render Hooks — useState, useEffect, useRef, useMemo, useContext Template engine — Vertex.template / Mustache Hash router — Backbone style useHash — router meets reconciler jQuery compatibility Quick API reference 01. Obtaining vertex.js Vertex is a 1kloc SPA framework containing everything you need from React, Ractive-Load and jQuery while still being jQuery-compatible. vertex.js is a single, self-contained file with no build step and no dependencies. Download it from the Gist below and drop it wherever your project serves static assets. gist Or fetch it from the command line: # save to your project's static directory curl -o static/vertex.js \ https://gist.githubusercontent.com/LukeB42/ef5b142325fc2bcd4915ba9b452f6230/raw/867cf609e8ec6bcb6700eda7f81c7c60e9ee01c9/vertex.js It ships as a UMD module, so it works equally as a plain <script> tag, a CommonJS require(), or an AMD define(). 02. Loading in the <head> Add a single <script> tag. Place it before any code that references Vertex or V$. <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>My App</title> <script src="/static/vertex.js"></script> <!-- If you also use jQuery, load it BEFORE vertex.js. vertex.js detects $ and leaves it untouched. --> <!-- <script src="/static/jquery.min.js"></script> --> </head> <body> <div id="root"></div> </body> </html> After loading, the following globals are available: Global Description Vertex Full namespace — all features live here V$ Shorthand DOM wrapper — always available $ Also set to the DOM wrapper only when jQuery is absent 03. Setting a templates directory Set Vertex.template.load.baseUri once at startup and every subsequent load() call that receives a relative path will automatically prepend it. Absolute URLs (starting with http://, https://, or /) are always used as-is, so fully-qualified paths continue to work unchanged. // main.js — set the base once, then use short names everywhere Vertex.template.load.baseUri = "/static/templates/"; // "user-card" resolves to /static/templates/user-card Vertex.template.load("user-card", { el: "#sidebar", data: { name: "Alice", role: "Engineer" } }).then(instance => { instance.on("change", e => console.log("changed:", e)); }); // Absolute paths bypass baseUri entirely Vertex.template.load("/other/path/special.html", { el: "#special" }); Vertex.template.load("https://cdn.example.com/tmpl.html", { el: "#remote" }); A template file at /static/templates/user-card.html is just a regular HTML fragment wrapped in a <template> tag: <!-- /static/templates/user-card.html --> <template> <div class="card"> <h2>{{name}}</h2> <p>{{role}}</p> {{#if email}}<a href="mailto:{{email}}">{{email}}</a>{{/if}} </div> </template> Note: If the file has no <template> tag, Vertex.template.load() uses the entire response body as the template string. Both forms work. 04. DOM layer — V$ / VQuery V$(selector) returns a chainable wrapper around a set of matched elements — identical in spirit to hn.js with a fuller jQuery surface. Every method returns this for chaining. Selecting & creating elements // CSS selector V$(".card").addClass("active"); // HTML creation const el = V$('<li class="item">Hello</li>'); // Scoped query (2nd arg = context) V$("li", "#my-list").each(function() { console.log(this.textContent); }); // Document ready V$(function() { console.log("DOM ready"); }); Events — .on() / .off() / .trigger() // Direct event binding V$("button").on("click", function(e) { V$(this).toggleClass("pressed"); }); // Multiple events at once V$("input").on("focus blur", function() { V$(this).toggleClass("active"); }); // Event delegation (bubbles up from ".row" to "#table") V$("#table").on("click", ".row", function(e) { console.log("row clicked:", this.dataset.id); }); // Remove handler const handler = e => doSomething(e); V$("#btn").on("click", handler); V$("#btn").off("click", handler); // Custom event dispatch V$("#root").trigger("app:ready", { version: "1.0" }); Attributes, styles & values // .attr(name) → get // .attr(name, val) → set (chainable) // .css(prop) → get computed value // .css(prop, val) → set style property // .css({ prop: val }) → set multiple // .val() → get input value // .val(v) → set input value V$("img") .attr("alt", "A scenic photo") .css({ borderRadius: "4px", opacity: "0.9" }); const username = V$("#name-input").val(); V$("#name-input").val("").attr("placeholder", "Enter name…"); Content & traversal // Content V$("#output").html("<strong>Done.</strong>"); V$("#label").text("Status: OK"); V$("ul").append("<li>New item</li>"); V$("ul").prepend("<li>First item</li>"); // Traversal V$(".panel").find("input").val(""); // clear all inputs inside .panel V$("li.active").parent().addClass("has-active"); V$("li").first().addClass("leader"); V$("li").eq(2).remove(); V$("li").filter(function(el, i) { return i % 2 === 0; }).addClass("even"); V$(".item").not(".disabled").on("click", handleClick); 05. AJAX Vertex.ajax() wraps the Fetch API with a jQuery-shaped surface: success/error callbacks, dataType, content-type handling, and .done()/.fail() on the returned Promise. // Full options form Vertex.ajax({ url: "/api/tracks", method: "GET", data: { genre: "bass", limit: 20 }, dataType: "json", success: tracks => renderTracks(tracks), error: err => console.error(err) }); // POST with JSON body Vertex.ajax({ url: "/api/session", method: "POST", contentType: "application/json", data: { token: myToken }, success: session => startSession(session) }); // Promise style Vertex.ajax({ url: "/api/ping" }) .done(res => console.log("ok", res)) .fail(err => console.warn("failed", err)); // Shorthand GET / POST Vertex.get("/api/user", data => console.log(data)); Vertex.post("/api/save", { title: "Mix A" }, res => console.log(res)); 06. Fiber reconciler — createElement & render Vertex's reconciler follows the same fiber architecture described at pomb.us. The public API is intentionally React-compatible. createElement const { createElement: h, render, Fragment } = Vertex; // Host element h("div", { className: "card" }, h("h2", null, "Hello"), h("p", null, "World") ) // Function component function Badge({ label, colour }) { return h("span", { style: { background: colour } }, label); } h(Badge, { label: "Bass", colour: "#c8ff00" }) // Fragment — renders children with no wrapper element h(Fragment, null, h("dt", null, "BPM"), h("dd", null, "174") ) render // Mount your root component once — Vertex handles all subsequent updates function App() { return h("div", { className: "app" }, h("h1", null, "Vertex") ); } Vertex.render( h(App, null), document.getElementById("root") ); Lazy / async components // Vertex.lazy() follows the React.lazy Suspense protocol. // The component is fetched once; Vertex re-renders automatically. const HeavyChart = Vertex.lazy(() => import("/static/js/chart.js")); function Dashboard() { return h(HeavyChart, { data: chartData }); } 07. Hooks All hooks follow React's rules: call them only at the top level of a function component, never inside conditionals or loops. useState function Counter() { const [count, setCount] = Vertex.useState(0); return h("div", null, h("span", null, String(count)), h("button", { onClick: () => setCount(c => c + 1) }, "+"), h("button", { onClick: () => setCount(0) }, "reset") ); } useReducer function reducer(state, action) { switch (action.type) { case "add": return { ...state, items: [...state.items, action.item] }; case "clear": return { ...state, items: [] }; default: return state; } } function Playlist() { const [state, dispatch] = Vertex.useReducer(reducer, { items: [] }); return h("ul", null, ...state.items.map(t => h("li", { key: t.id }, t.title)) ); } useEffect function AudioPlayer({ src }) { const audioRef = Vertex.useRef(null); // Run once on mount — teardown on unmount Vertex.useEffect(function() { const ctx = new AudioContext(); audioRef.current = ctx; return () => ctx.close(); // cleanup }, []); // Re-run when src changes Vertex.useEffect(function() { if (audioRef.current) loadTrack(audioRef.current, src); }, [src]); return h("div", { className: "player" }, "Playing: " + src); } useMemo & useCallback function TrackList({ tracks, filter }) { // Only re-computed when tracks or filter changes const filtered = Vertex.useMemo( () => tracks.filter(t => t.genre === filter), [tracks, filter] ); // Stable function reference — safe to pass to child components const handleClick = Vertex.useCallback( t => console.log("selected:", t.title), [] ); return h("ul", null, ...filtered.map(t => h("li", { onClick: () => handleClick(t) }, t.title) ) ); } useRef function FocusInput() { const inputRef = Vertex.useRef(null); Vertex.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []); return h("input", { ref: inputRef, // Vertex will write the DOM node here placeholder: "Type…" }); } createContext & useContext const ThemeCtx = Vertex.createContext("dark"); function App() { return h(ThemeCtx.Provider, { value: "dark" }, h(Toolbar, null) ); } function Toolbar() { const theme = Vertex.useContext(ThemeCtx); return h("nav", { className: "toolbar theme-" + theme }); } 08. Template engine — Vertex.template / Mustache The Vertex.template constructor takes an element target, a mustache template string, and a data object. It renders immediately and re-renders on every .set() or .update(). Basic usage const r = new Vertex.template({ el: "#app", template: ` <h1>{{title}}</h1> <ul> {{#each tracks}} <li>{{@index}}. {{name}} — {{bpm}} BPM</li> {{/each}} </ul> `, data: { title: "My Set", tracks: [ { name: "Vortex", bpm: 174 }, { name: "Subsonic", bpm: 140 }, ] } }); // Update a single key — triggers re-render r.set("title", "Night Set"); // Merge multiple keys at once r.update({ title: "Morning Set", tracks: [] }); // Listen for data changes r.on("change", ({ keypath, value }) => { console.log(keypath, "→", value); }); Mustache