TodoMVC App Written in Vanilla JavaScript
github.com
github.com
If anything, modern frameworks tend to build as much upon the native functionality that is available. This reduces bundle sizes and increases performance. It's an abstraction on top of the browser capabilities, not a clone.
Now, onto the code itself. The author claims it is "pretty simple" to build "fairly complex things". He backs up the claim by stating it took only 60 minutes to write this app.
First, I would be careful to claim things are easy or difficult. This is very subjective, and phrased in the wrong way it can also come across as arrogant.
Second, the app is not simple. Looking at the code the author created his own DOM abstraction. The current abstraction is only proven to work for a simple to do app.
Third, a to do app is not a "complex thing". Not even remotely.
It's a nice exercise, but the conclusion drawn by the author aren't valid.
Exactly. To elaborate, here's the big, big caveat in a miniature form:
https://github.com/1Marc/todomvc-vanillajs-2022/blob/854dd40...
Note that this clears and recreates the DOM on every action (eg mark complete), and DOM ops are the most expensive ones. This is a sound strategy for vanilla JS, because the alternative is mutating the DOM and keeping it in sync which is error prone to put it mildly.
I use frameworks very sparingly, and am always cautious with deps. But a reactive UI/diff engine is non-negotiable for me.
It still has the same problems you describe. You worded it way more eloquently than I can.
A good part.
and after that you only need to manage state
A part where to change a[n].b.c:
React: you either split into unhealthy number of “components”, which are never reused and require tons of pass-through, or only manage state as a sequence of unnatural to js “reduce” operations.
Vue: give away your data to Vue, because once it hits `.data` it gets charged with properties incompatible with the entirety of js language.
Vue is essentially a separate programming language, which somehow managed to sell the manual transpilation to its users. React did the same being Haskell-like variant. The “upon native functionality” of this is equivalent to python3.dll being upon a native functionality of C++.
Most of your points still valid, I only wanted to unfold that “only manage state” bit. It’s sad that libraries like Mithril aren’t the default, and instead monstrosities like React are.
I would encourage you to find out why React is popular and why Mithril isn't on the assumption that the world isn't crazy.
I honestly don’t know if that’s the point of the author but I would agree that many framework are too heavy, but honestly there so many good compromises today that don’t make you re-invent the wheel and fix bugs that have been have been fixed millions of times before.
Going against good abstractions as developers seems to go against the most powerful tool we’ve got.
One thing that's easy to notice from reading the code is that so much of the code takes inspiration from the frameworks themselves. How the code is organised - the state store, the render function, the computed getters, the mutation methods, it's all there. It's clean.
Perhaps the main benefit of frameworks is that they've taught a whole generation of JS developers how to organise their code in a way that can scale to bigger, more complex apps (vs. how easy to make a mess of things in the jQuery days).
-
Having said that, I do agree with your point about things getting more complex: This is a properly specced, easy to grasp project. Throw in stakeholders and PMs needing to add functionality and requirements changing, and this system might start to struggle. Not to mention the cost of onboarding new developers. I feel like that's the best thing about frameworks in any language; when you jump into a new project you know more or less how everything is organised and where things are supposed to be, and you can get going fairly quickly.
This rewrite of the Vanilla JS TodoMVC is a great illustration of how far things have come.
Save the element template as a string and set innerhtml of a div as that?
> compromises today that don’t make you re-invent the wheel
Yeah but there's also no need to include jQuery and bog down your site with piles of code you won't ever call just because you don't know how to use the native api like a normal person. React/Angular/whatever are just the latest fad of that mindset, big team corporate ease of use aside.
So yes, this pattern is helpful for solving that sort of thing when it occurs (and if the use case is even substantial enough to warrant it) and Lit looks like it would fit that role reasonably well, but basing the entire site on this principle as a full on framework seems a bit ludicrous.
What html actually lacks is the option to include external html part files, but there are lots of ways to do that serverside that don't involve the stupid idea of putting html into strings in js files for literally everything.
Had no idea who was behind it, but I love that they're still contributing back like this.
Demo: https://todo-react-redux-noscript.herokuapp.com/
Repo: https://github.com/wishy-gift/todo-react-redux-noscript/
Talk (updated framework): https://m.youtube.com/watch?v=3yY-Z-X3xE4
Helpers to do your own stuff: https://www.npmjs.com/package/@wishy-gift/noscript
While I would rather nail my bollocks to the ceiling than build a vanilla application (in any stack), there’s no denying the benefit revisiting the basics.
I'd love to not be able to use them and to create complex UIs in pure JavaScript but it always ends up being painful where you spend more time optimising the rendering than doing any work.
In simple apps I will write UI in a functional matter in pure JavaScript, regardless of the performance implications. But bigger DOM means worse performance with this method.
When will we get to the point where we can tell the browser what we want the DOM to look like and then the browser does the diffing like a framework would?
Remember <meta http-equiv="refresh" content="5"> .
It was discouraged because the back button broke. If you're developing a single page react app, I don't think you care about the back button.
You should though. One easy way is to use react-router. e.g. going to /profile renders the Profile component. Then navigating to /dashboard renders the Dashboard component. Pressing the back button will take you to /profile.
Another pet peeve of mine is people using internal react state when they should just use path or query params to make it transparent and easily sharable.
Your users likely do, so you should too.
There's sort of a "Maslow's Hierarchy of User Needs" that exists in software though. So long as the user can do something, and their immediate needs are met, performance concerns are just a small annoyance. And generally they will continue to ask for more features rather than performance improvements as long as the app remains usable.
Not sure where you think mutation comes into play here, that's an orthogonal concern—you can have an API that mutates some internal representation of a VDOM, and it's still faster than manipulating the DOM directly.
It makes sense to be able to describe what you want to achieve and to let the browser determine how to get there in the most efficient way.
If the browser could do this it would make a tonne of JS code so much simpler. There's a lot that goes into making sure that there's no unnecessary DOM updates because it's just slow.
I don't write assembly and move things between registers by hand, I write a function and the compiler decides the most efficient way of doing it. UI should be the same way, I say what I want to happen and the browser decides how to get there.
This is also something that can be automated by a framework. Check out https://svelte.dev/blog/virtual-dom-is-pure-overhead
Whether that's Svelte without a VDOM or React with a VDOM. The entire point of these frameworks is that you can write complex UI in a declarative way. They both exist because writing it declaratively in pure JS leads to performance issues. But there's nothing stopping the browser implementing something that does this optimisation for you.
If you want to write declarative UI in pure JS then you need the browser to do these DOM optimisations for you, and diffing the changes is the way to do that.
The main bottleneck here are the synchronous localStorage calls happening in the render path, they could be moved to a separate timer.
I still suspect this might be faster than React anyway. Would be interesting to try.
Read my comment again.
It's normal to get much worse performance than React if you layout thrash and it's very easy to layout thrash without batched updates. There's overhead in the vdom but getting all the updates batched tends to be a performance win in any decent sized app. This was an active area of library exploration/development in the 2008-2012 timeframe but the only framework actively promoting it was Sencha IIRC.
- the UI isn’t a source of truth, it’s a computed interface to it
- changes/interactions in the UI should update the source of truth, not other parts of the UI
- the source of truth is fully responsible for managing and notifying dependencies of relevant changes
Or much shorter: state and presentation are separate concerns. Once you have that, you can focus on the actual problems you’re trying to solve rather than the ones created by crossing underlying technology boundaries.
Maybe not “should”, but it’s an incredibly helpful mental model, and I can think of very few cases it can’t model correctly. It’s so helpful a model that it’s gained traction in Rust, Swift, Kotlin, Dart, Go. It’s the ~only model in Clojure and and literally the foundation of Elm.
None of these cases, AFAIK, apply the model literally when they interface with inherently imperative APIs. They provide APIs expressing the model, and manage the imperative behavior on your behalf. This is ultimately how all functions in meaningful programming work. At the end of the day, allocating memory or displaying output is a side effect.
This is also how you can have custom renderers like those used in ink, three-solid, even React Native. The code is modeled as a declarative function of state, implemented as a set of imperative behaviors corresponding to state and dependency changes.
I’ve worked on contentEditable solutions in the past, probably more than most people who didn’t ultimately produce a library from the work (I went mad, literally angry at the complexity of making it a good UX, and gave up). I hope I never have that task again, but if I do… you can bet your ass I’d use a function of state model. It’s the only way I could reason about the problem without going mad.
React and other declarative approaches are inherently different. In React I hardly think of when my component renders. In Backbone days, I remember having to debug why some part of code is not running when I'm expecting it to run. React does this really well.
It can get confusing to track down unexpected renders when you think the result of the algorithm should be different, but that’s not really an issue with react so much a the nature of managing complex state, memoization algorithms which potentially use different diffing strategies, and logic which might be mutating state in ways you don’t quite expect, and so on.
People criticize these front end libraries but I’m still impressed by how well state management has been integrated into such extensible and scalable view layers.
Would you need a “framework” when an app gets beyond TODO? I think you would need to start refactoring your code to be more framwork-like. But this is something we don’t do, and I would say would even be considered “anti-pattern”.
HTML and CSS are actually quite nice to work with, given modern browser developer tools. Much simpler to work with when they are literal files.
It wouldn’t surprise me if people continue to compile other languages to JavaScript or WASM though. It seems reasonable a Java developer might like to work in Java or a Ruby developer in Ruby instead of writing JavaScript. But I don’t understand how people feel confident writing web applications if they don’t take the time to understand CSS, HTML, or the browser APIs.
0: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Typ...
1: https://github.com/1Marc/todomvc-vanillajs-2022/blob/1c31309...
2: https://developer.mozilla.org/en-US/docs/Web/API/TrustedHTML...
The entire TODO list is rendered anytime a single item is changed/removed/added. The biggest thing frameworks give us is fast, differential DOM updates.
Once browsers add differential update APIs, then we can kiss React and all the other players goodbye :-)
What I will say is it’s tough. I don’t like the idea of react at this point. I like building products that don’t need something heavy like react/redux.
I recently launched https://lists.sh to scratch that no js itch. It was a ton of fun and I want to chase that feeling.
But building a web app is what businesses want. And while you could make vanilla js work, you’d be reinventing a lot of tooling to get you there.
- add time constraints, where you need to implement widgets that you could source from a JS framework ecosystem otherwise.
- in your 10 engineers, make it 5 juniors, 2 interns and 3 seniors comming from PHP, and make sure they write all the code in the same style, follow a congruent architecture, can find solutions to common problem, get easy onboarding, etc.
- request sub-routing, off-line mode, notifications, content udpated by multiple users and so on.
- put a big table in there with 10000 rows and ask your team it renders fast.
I’ve made millions and millions for major brands, including my own company, and customers go “wow how is your site so fast!!” All. The. Time.
The biggest pressure I’ve found is from the engineers themselves. Not all engineers like to go counter culture and learn browser APIs and JavaScript. They would simply like to use what everyone else is using. They tend to argue against vanilla using canned arguments read off of framework websites that don’t apply much or aren’t even problems in the actual codebase.
:shrug:
> App.$.filters.querySelectorAll('a').forEach(el => el.classList.remove('selected'));
It's older though (seems to have been first written 6 years ago) and has significantly more code.
Uncaught SyntaxError: Missing initializer in const declaration (at helpers.js:4:14)
Installed and ran with: npm install
npm run start
Any idea what went wrong...?I think it hides the complexity by pretending that's not a problem
While events aren’t explicitly mentioned, MDN offers an insightful read:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memo...
Alternately, one can use event delegation [0]. As does React under the hood [1].
0 - https://javascript.info/event-delegation
1 - https://reactjs.org/blog/2020/08/10/react-v17-rc.html#change...
Also the browser doesn't make a distinction to the type of event (such as `click` ), and say "Oh well you can't click on an element not in the DOM, I think I can clean this up"
So no it will not clean it up and it will leak memory, see for yourself
- Use esbuild to bundle the app. - Now you can change the file extension and switch to Typescript. (Esbuild doesn't check the types, but VS Code will.) - Then change the file extension to tsx and use Preact.
I'm writing a little app this way and it seems quite nice. I don't think I'm missing anything?
Before even considering something like this I’d want to see a style guide and clear definition of where code should live. Frameworks like React do that well. I can expect a random React dev to mostly do similar things.
This has not been my experience. Even experienced software devs will differ stylistically, but intermediate devs often come up with very different ideas about how things ought to work. I suspect this is due to wide variance in awareness of different libraries/platform features/stylistic patterns/etc, a consequence of being in a relatively unregulated industry.
I have worked for companies using React since 2014 and no 2 projects have had the same layout. My current and previous employers have had the closest codebases, but only in terms of directory structure, and even now the current one is changing to become quite different (in a way I think is good, but only time will tell).
I'm glad we have frameworks.