Website | VanillaJS Toolkit (ignore the homepage, look at the menu at the top) | VanillaJS Components
"A Framework Author's Case Against Frameworks": Adrian Holovaty, dotJS 2017
brings to mind Vlissides' pattern of evolving frameworks*
Also brings to mind the Windows++ story: build your own to understand what's going on underneath--not quite the same point as this author, but certainly a viable line of thought/approach.*
Drawbacks of frameworks:
The Framework Tax: frameworks require you to:
comply with their API so that they can provide your their services. This is just the way a framework works: your code will have to adhere to some rules, including more or less boilerplate code. So it’s the framework way, or the highway. Your daily challenges will be less about “how to do this” than “how to make the framework (not) do this”. Dodge those constraints at your own risks: if you bypass a framework by directly calling low-level APIs, don’t expect it to understand what you’re trying to do, don’t expect it to stay consistent. So it’s a false promise frameworks make that you’ll be “focusing on your business”: in reality you have to care on the framework too, and a lot.
upgrades are effectively forced if you:
1) want a new feature (even if you didn’t wanted all those of the next release, you need to upgrade the whole thing) or
2) want a bugfix, or
3) want to avoid loosing support (as new versions are shipped, the one on which you have based your app will get deprecated).
Upgrades can also be lacking and let you frustrated (and possibly with a project at risk) with an identified bug but no planned date for a fix. Third-party framework-specific libraries (such as widgets) or plugins are no exception to that rule and will be less and less compatible with your app if you keep using old versions. Maintaining backward compatibility has became such a hassle for frameworks maintainers that they now find more profitable to work on tools that automate upgrades of your code as much as possible (Angular’s ng-update, React native Upgrade helper, Facebook’s jscodeshift, etc.).
train to learn how they work (what they can/cannot do, what are their concepts, APIs, ecosystem, tools), including changes that may occur in new versions. Should you pick the most popular framework of the day, this might be easier, but it’s unlikely that you’ll ever know about every aspects of a given framework. Also, hype comes and goes: should you decide to use another framework for a new app (or even worse, to migrate from one to another), the cost of investing in such proprietary knowledge will be lost. This explains a lot of inertia in enterprise projects, even if each project is different than the previous one. “Compatibility means deliberately repeating other people’s mistakes,” said the late David Wheeler.
compromise with the drawbacks implied by delegating control: you may not be able to do whatever you want (or to prevent the framework from doing things you do not want) or you may not achieve the performance you want (because of additional layering, too-generic code, bigger code size or backward compatibility requirements).
lose skills. A number of developers either don’t know much about the lower-level APIs (because they always used the framework layer instead) or live in the past (i.e. are stick on an outdated knowledge of it, not being aware of the latest improvements and new capabilities). The law of the instrument then leads too often to build overkill solutions to simple problems, and loose (if even once acquired) knowledge to build simpler ones. Being guided by blueprints and recipes, they loose (or not gain) a culture of good software design (principles, patterns) and barely build a significant engineering experience. Just like users of CSS frameworks (Bootstrap, Tailwind, etc.) lack CSS skills, users of web frameworks are doomed to lack experience in both modern web APIs and software design in general.
The Framework Silo: Aside the “tax” that you have to pay to get their benefits, frameworks can also induce an additional major issue when they are not standard. As they enforce rules — but each one of them is different — this implies binding your app with a proprietary ecosystem. That means locking your app code with a proprietary API (and its upgrade process). That’s a risky bet for your project.
Are languages (CoffeeScript, TypeScript, Dart, etc) subject to the same problems as frameworks? Yup.
Frameworks are a good thing if they:
So, in a nutshell, avoiding a framework to build an app aims to:
Building an app without a framework:
Goals and mindset: We must clarify the anti-goal: “building an app without a framework” is NOT to be confused with “replacing the framework”. This is not the challenge at stake: a framework is a general purpose technical solution to host virtually any app, so it is less about your app than all apps. On the opposite, going vanilla is an opportunity to focus on your app’s needs only.
This is an important scope narrowing to make to assess the (non-)difficulty of building your app without a framework: it is not as hard as building a framework, because you do NOT aim to build:
So building a vanilla app is not an enormous task of “reinventing the wheel” as often caricatured, because the major part of this “wheel” is actually about the APIs/contracts, their implementations, the general-purpose engine and associated optimizations, the debugging capabilities, etc.. Leaving the general-purpose goal and focusing on your app’s goals means that you can rid of most of it. Ironically, this is the real “focus on your app” approach.
change your state of mind: don’t look for the framework-specific services mentioned above. As a vanilla app, you will probably don’t need it. Don’t think change detection, just update the DOM, etc.
use technical alternatives for the common tasks you performed with frameworks (updating the DOM — including reactively — , loading lazily, etc.)
Standards: standards APIs are among the “good frameworks” as they:
What about the use of libraries? As for the “rewrite a framework” false assumption, it is often considered that vanilla JS apps are NOT supposed to use libraries. This is utterly false. Once again, “reinventing the wheel”, i.e. rewriting everything from scratch cannot be a sensible goal. The vanilla goal of removing constraints implied by frameworks and not libraries, must not be confused with a “write everything by yourself” dogma.
(This raises an interesting question not answered by the author: What is the difference between a framework and a library?)
(THis is a fascinating throwaway line at the end of this section:) don’t be fooled by frameworks documentation or articles that would claim that they are not a framework (because they would be “unopinionated”, or not defining a “complete application“, etc.): as soon as they imply a contract, they are. (Can libraries be opinionated?) (NOTE: author also has another post about the differences between libraries and frameworks in which he summarizes the two as "frameworks provide you application blueprints with built-in services but enforces predefined contracts to call your code and thus imply a strong dependency; libraries won’t help you design your application, but can be called only when you need them. You can devise a design that limits dependency to them.")
Patterns. Just use of patterns is not enough, but it helps allow people coming to your app consume it more quickly (lighter cognitive burden).
Concerns:
Updating: When interviewing developers about what would be their primary concerns when trying to build a vanilla application, most of them reply that it would be complicated to implement model change detection and subsequent updates in the relevant “views” of the app. This is a typical law of the instrument effect, which makes you think in a framework way, whereas not being a framework actually implies much more simple needs:
Templates: Another feature that developers fear to miss is the ability to write HTML snippets with dynamics parts, even listeners, etc.
First of all the DOM API (document.createElement("button"), etc.) is not that hard, and actually more powerful than any template language since this allows you full access to the API. It can be tedious to build long HTML fragments but, hey, if they are that long, it’s probably that you need to split it in more fine grained components.
It is true, however, that viewing those elements as a template improves readability. So how to have them? There are actually multiple ways:
HTML Templates are now available in browsers (since 2017 at worse). They provide the ability to build a reusable, off-screen, HTML
snippet. This is this actually a part of Web Components and yes, they can support transclusion through
Template literals are available in JavaScript since ES6 (2015). They allow you to easily embed values inside a string. That be enough to embed primitives (numbers, strings, including other HTML code, etc.) but not more sophisticated elements like DOM elements on which you registered listeners for instance.
A tagged template literal function can help embedding complex values like DOM Nodes into such a template that would result in a Node itself. ObservableHQ has devised a pretty handy one that allows you to write things like html<header>${stringOrNode}</header> or to do more complex templating like html
What about conditionals or loops in a template? Aside the fact that this might have never been a good idea (UIs should be dumb and not contain logic), you can (and should) just do it JS, then insert the result in your template, using one of the techniques above.
Events: how to bind events to DOM Nodes inside them? There are also several alternatives:
Now what about custom/business events? What if I need to react to some event triggered by a component of my app. There are also multiple options for that:
Components: it is still a good idea design software items as reusable (i.e. context-independent) if they can occur multiple times in your system. Whatever the technology you use, well-grained abstractions remain useful, whether they are business or technical: it’s always a good idea to group data and rules pertaining to the same business concept into a reusable object or to build a widget that can be instantiated in multiple places in your app.
Aside [from] standard widget components (which would typically be implemented as standard Web Components), any component should typically be able to (standard component things: methods, state, events):
In any case, whatever the design strategy you choose, you component (or more specifically its associated “view”) must be able to provide some HTML rendering result. A string containing HTML code could be used, but an actual HTMLElement (or just Element) is usually a better choice (easier to read/update rather than parsing, allowing to bind event handlers on it) and a more performant one (no parsing required).
Also, you might want to use external components from third-party libraries. For sure, proprietary frameworks, because of their popularity, benefit from a larger number of libraries and components developed by their community. Although most of them are actually not so different from what they would be if they’d had been implemented in pure Javascript (like this was the case at JQuery times) they do suffer from lack of interoperability, and you find yourself looking for either vanilla or Web components.
Hopefully such libraries exist, such as the Vanilla JS Toolkit, even if less common. Regarding standards ones, WebComponents.org list 2000+ elements. There’s even vanilla web components, but the vanilla aspect is less relevant here (more about implementation lightweightness than interoperability).
Routing: Managing routes in a SPA today requires using the web History API. Whereas this is less complex that you imagine, you might want to delegate this to a simple router library such as Navigo. All you have to do, then, is to replace an DOM element by another (using replaceChildren() or replaceWith()) when a route is reached.
Lazy loading: ES6 (2015) can load modules dynamically. This works in Node, but in browsers too: {WelcomeModule} = await import("./welcome/ModuleImpl"); module = new WelcomeModule()
Bundlers like Webpack can insulate modules in dedicated files. Beware that you should only use constants in the import path, though: otherwise, the bundler cannot guess what you will load and will package the whole set of possible files in a single chunk. For instance await import(
./welcome/${moduleName})
will bundle everything in the specified directory, given that your bundler doesn’t know what the moduleName
variable will hold at runtime.
Native apps: (as opposed to using framework derivatives like React Native)
Server-side rendering (SSR): (I find it hilarious and sad that we talk about using client-side Javascript fameworks to do server-side rendering with a straight face)
Internationalization: Internationalization have been handled by libraries for many years now (and finally integrated within frameworks). You can easily integrate one of those libs but this could also be a good candidate for an in-house implementation, which would allow more simple and efficient messages types than a general-purpose lib can.
```
interface WelcomeMessages {
title: string
greetings(user: string, unreadCount: number): string
}
class WelcomeMessage_en implements WelcomeMessage {
title = "Welcome !",
greetings = (user, unreadCount) => `Welcome ${user}, you have ${unreadCount} unread messages.`
}
class WelcomeMessage_fr implements WelcomeMessage {
title = "Bienvenue !",
greetings = (user, unreadCount) => `Bienvenue ${user}, vous avez ${unreadCount} nouveaux messages.`
}
```
Note that this provides you:
type checking: every message has a static type (and several implementations/translations), so your IDE can check if you’re using a valid message property or not, and provide you auto-completion.
translation exhaustivity check: you can’t compile until all interface keys are implemented in all languages.
All you have is to (load and) instantiate the message class that is relevant to you user’s locale. General purpose libs can’t provide such business-specific types.
(I think he's oversimplifying the case, but at the same time, I think a lot of frameworks over-complexify the case so let's assume this is the simplest thing, and get more complicated from there as need be.)
Tools: If you want to free yourself from dependency on a too constraining software stack, it is likely that you’d want to do the same with your tools: you don’t want to depend on them (their limitations, their performance, their bugs, their versions) to be able to move forward. You don’t want tot get stuck with a build problem that you cannot solve (or need hours or days to solve) because you do not own it (especially when using trendy but recent build tools which are not fully battle-tested yet). That said, you will hardy avoid those tools. Most often your production code will have to be bundled in a clever way, involving minification, obfuscation, code splitting, tree shaking, lazy loading, style inclusion, etc. and there is little doubt that existing packagers such as Webpack, Parcel, ESBuild or Vite will do it better that you could. All you can do about it is:
(I'm really not sure I agree with his take on this; he has some good points, but this feels like it encourages a very "build it all yourself" approach--which, to be fair, is a known concern that he's addressed repeatedly all over the place. Still, I think the better takeaway here is, "Use tools sparingly but deliberately." Know what the tool does, be able to live one layer deeper than what the tool gives you, and so on.)
Challenges: (from others)
Last modified 28 April 2025