Ever had a “simple” frontend tweak turn into npm install roulette… and now you’re babysitting 40 transitive dependencies for a dropdown? Yeah. Same.
The good news is a bunch of the stuff we reach for libraries to do is just patterns sitting on top of Web APIs we already have. The Vanilla JS Patterns That Replace Entire Libraries approach isn’t about being anti-library. It’s about knowing what the browser already gives us, and using it cleanly—so we add weight only when it buys us something real.
Key Takeaways
- Many popular libraries are wrappers around stable Web APIs like
fetch(),CustomEvent, and the History API. - Event delegation +
Element.closest()often replaces jQuery-style “bind handlers everywhere”. fetch()+AbortControllercovers most “Axios-like” needs, including canceling in-flight requests.- A tiny observable store + a single
render()function can replace Redux/Zustand for a lot of apps. - Middleware is a pattern (not magic). You can implement a pipeline in a handful of lines.
- A small router using
history.pushState()or hash routing can replace heavy client routers.
Why “Vanilla JS patterns” beat “yet another dependency” (sometimes)
The README of quantuminformation’s Vanilla.js Patterns repo makes a point i agree with: frameworks can become a “black box”, and frequent updates can turn maintenance into a chore. Using vanilla patterns keeps things transparent and tied to the platform—JavaScript + Web APIs—so the knowledge transfers everywhere.
Source: https://github.com/quantuminformation/vanillajs-patterns
Also, modern browser support is pretty solid for a lot of these APIs:
AbortControllerhas been available across browsers since March 2019 (MDN).Source: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
Element.closest()has been available since April 2017 (MDN).Source: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
CustomEventhas been available since July 2015 (MDN).Source: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
So in 2026, “vanilla” doesn’t mean “primitive”. It often means “less glue code”.
Vanilla JS patterns: Event delegation (replace jQuery-style handlers)
If you’ve ever done “attach a click handler to every button”, you’ve already felt the pain:
- lots of listeners
- newly inserted DOM nodes don’t have handlers
- code gets scattered
Event delegation flips that. One listener on a parent + closest() to figure out what was clicked.
document.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action='delete']");
if (!btn) return;
const id = btn.dataset.id;
deleteItem(id);
});This pattern replaces a surprising amount of “jQuery for events” usage. And closest() is made for this: it walks up the DOM tree until it finds a matching selector (or returns null).
Docs: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
In practice: i use this for tables, lists, menus—anything repeated. One listener. Done.
Vanilla JS patterns: fetch() + a tiny wrapper (replace Axios)
MDN is blunt: Fetch is the modern replacement for XMLHttpRequest and it’s promise-based.
Docs: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
What most teams really want from Axios is:
- JSON defaults
- error handling
- cancellation / “ignore previous request”
- maybe timeouts
You can cover that with a small wrapper:
export async function httpJson(url, { method = "GET", body, signal } = {}) {
const res = await fetch(url, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
signal,
});
// fetch() does NOT reject on 404/500, so we check:
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}Cancel in-flight requests with AbortController (the “typeahead” fix)
AbortController lets us cancel a fetch when a new one starts—perfect for search suggestions.
let controller;
async function search(q) {
controller?.abort();
controller = new AbortController();
return httpJson(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
}MDN notes AbortController.abort() can abort fetch requests and even stream consumption.
Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
Vanilla JS patterns: A tiny store + one render function (replace Redux/Zustand)
A Reddit thread on organizing vanilla JS basically describes the moment we all hit: “my script.js is getting long, state is scattered, and i’m writing an update() function to keep DOM in sync.” That instinct is good. You’re reinventing unidirectional state → render.
Source: https://www.reddit.com/r/learnjavascript/comments/17i3zdw/learning_to_better_organize_vanilla_javascript/
Here’s a minimal store (observer pattern). This is the same family of idea as what “5 Vanilla JS Patterns That Replace Entire Libraries” argues: you can often do this in ~30 lines.
Source: https://javascript.plainenglish.io/5-vanilla-js-patterns-that-replace-entire-libraries-3d22445bfbc0
export function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (patch) => {
state = { ...state, ...patch };
listeners.forEach((fn) => fn(state));
},
subscribe: (fn) => (listeners.add(fn), () => listeners.delete(fn)),
};
}
// usage
const store = createStore({ count: 0 });
function render(s) {
document.querySelector("#count").textContent = s.count;
}
store.subscribe(render);
render(store.getState());
document.querySelector("#inc").addEventListener("click", () => {
store.setState({ count: store.getState().count + 1 });
});This won’t replace Redux for every app. But for “a few pieces of state reflected in the DOM”? It’s clean, testable, and doesn’t drag in an ecosystem.
Vanilla JS patterns: Middleware composition (replace middleware libraries)
Ignatius Sani’s “Stop Using Middleware Libraries” point is the one i wish more people internalized: middleware isn’t a feature; it’s a pattern—and you can implement it in ~10 lines.
Source: https://javascript.plainenglish.io/stop-using-middleware-libraries-you-only-need-10-lines-of-vanilla-js-b392f93afc4d
Here’s a compact compose:
export const compose = (...mws) => (ctx) =>
mws.reduceRight(
(next, mw) => () => mw(ctx, next),
() => Promise.resolve()
)();
// example middleware
const log = async (ctx, next) => { console.log("in", ctx); await next(); };
const auth = async (ctx, next) => { if (!ctx.user) throw new Error("nope"); await next(); };
await compose(log, auth)({ user: { id: 123 } });Use it for:
- request pipelines
- plugin systems
- validation chains
No “Manager class” required. (Those tend to get weird fast.)
Vanilla JS patterns: Routing with History API (replace client routers)
If you’re building a small SPA, you can often get away with:
history.pushState()for navigation- a route table
popstatelistener for back/forward
MDN covers pushState() and how navigation triggers popstate.
Docs: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
And if you need “works on GitHub Pages with no server config”, the Vanilla.js Patterns repo even calls out hash-based routing as a no-server option.
Source: https://github.com/quantuminformation/vanillajs-patterns
Minimal sketch:
const routes = {
"/": () => showHome(),
"/settings": () => showSettings(),
};
function navigate(path) {
history.pushState({}, "", path);
renderRoute();
}
function renderRoute() {
const path = location.pathname;
(routes[path] ?? show404)();
}
window.addEventListener("popstate", renderRoute);
document.addEventListener("click", (e) => {
const a = e.target.closest("a[data-nav]");
if (!a) return;
e.preventDefault();
navigate(a.getAttribute("href"));
});
renderRoute();That replaces a lot of router usage when your needs are basic.
Vanilla JS patterns: Event bus with EventTarget + CustomEvent (replace EventEmitter)
A clean way to decouple modules is an event bus. In the browser, we already have event infrastructure. CustomEvent lets you attach data via detail.
Docs: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
export const bus = new EventTarget();
export function emit(type, detail) {
bus.dispatchEvent(new CustomEvent(type, { detail }));
}
export function on(type, handler) {
const fn = (e) => handler(e.detail);
bus.addEventListener(type, fn);
return () => bus.removeEventListener(type, fn);
}
// usage
on("toast", ({ message }) => showToast(message));
emit("toast", { message: "Saved!" });It’s boring. That’s the point.
Bonus: “Build a library” patterns (when you really don’t need one)
Saurav Tiru’s series on baking a JS date library shows a very “old-school” but useful skill: building a small library with ES Modules, then using prototypes, method chaining, and even immutability (returning new instances instead of mutating).
Source: https://medium.com/@sauravtiru/bake-a-javascript-library-using-vanilla-js-part-3-21a5002c52eb
I’m not saying “write your own date-fns”. But the technique matters: small, focused utilities often age better than a dependency you never fully understood.
Conclusion
The real trick with Vanilla JS Patterns That Replace Entire Libraries is knowing which problems deserve a library and which are just a handful of solid Web APIs plus a proven pattern.
If you want a practical next step: pick one thing you currently depend on (events, routing, state, HTTP). Replace it in a small side project using the snippets above. You’ll learn the platform faster, and your codebase will feel a lot less… fragile.
And if you’re already using a framework, that’s fine too. These patterns still help inside React/Vue/Svelte apps because they’re just good JavaScript.
CTA: Try swapping one dependency this week (Axios, EventEmitter, a middleware helper). If you hit a weird edge case, leave a comment with your constraints—i’m happy to help sketch a “keep it vanilla” approach.
Internal link: If you’re experimenting with modern frontend stacks, you might also like my notes on adding AI features to a TanStack Start app: https://www.basantasapkota026.com.np/2026/03/adding-ai-features-to-my-tanstack-start.html
Sources
- quantuminformation, Vanilla.js Patterns (GitHub): https://github.com/quantuminformation/vanillajs-patterns
- Ignatius Sani, Stop Using Middleware Libraries. You Only Need 10 Lines of Vanilla JS: https://javascript.plainenglish.io/stop-using-middleware-libraries-you-only-need-10-lines-of-vanilla-js-b392f93afc4d
- Ignatius Sani, 5 Vanilla JS Patterns That Replace Entire Libraries: https://javascript.plainenglish.io/5-vanilla-js-patterns-that-replace-entire-libraries-3d22445bfbc0
- Saurav Tiru, Bake A Javascript Library using Vanilla JS (Part 3): https://medium.com/@sauravtiru/bake-a-javascript-library-using-vanilla-js-part-3-21a5002c52eb
- Reddit, Learning to better organize vanilla javascript (r/learnjavascript): https://www.reddit.com/r/learnjavascript/comments/17i3zdw/learning_to_better_organize_vanilla_javascript/
- MDN, Using the Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
- MDN, AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- MDN, Element.closest(): https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
- MDN, CustomEvent: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
- MDN, History.pushState(): https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
- Danny Moerkerke, The Lost Art of Vanilla JavaScript (referenced research): https://betterprogramming.pub/the-lost-art-of-vanilla-javascript-c11519720244