If you spend a lot of time on Hacker News, it’s easy to get taken by the allure of building a web app without a framework. There are a bunch of potential advantages (no bloat! bespoke to your project!) and being able to say you built something with minimal dependencies gets you Engineer Points.
That is, if you can pull it off.
I started a new side project recently. It’s a web-based graphics editor, so it needs to be a single page app. My time spent profiling SongRender for performance issues has made me a little wary of React for building interfaces that update at 60 frames per second, so I decided to avoid it. I’d go (mostly) vanilla and see how far that took me.
I installed lit-html and got to work. “Components” were simply functions that returned lit-html template results. A big singleton at the top of the tree held onto all the application state, stored as a global variable within that module.
The first hurdle came when a component needed local state. I could have lifted it to the singleton, but that would have broken the component’s encapsulation. I noticed that lit-html directives can keep state, so I decided to use them to build a tiny component library — ignoring a warning from the lit-html developers that this wasn’t a supported use case.
My home-brewed library worked great… until I needed to run some code when a component appeared on the screen. I started digging through lit-html documentation and issues looking for a way to detect a directive’s lifecycle, but it became clear to me that going down that path would be painful.
At that point, I recalled this quote by Tom Dale:
Fair enough. Let’s avoid that trap. What about a small framework? I’d heard a lot of good things about Svelte, and although I was slightly worried about the size of the Svelte community I figured it would be fine.
My migration attempt quickly ground to a halt when I wanted a parent component to apply some styles to a child component. In React, I’d pass a class name in from the parent as a
className prop. In Svelte, that’s considered a workaround, and the actual feature is the subject of an RFC.1
Maybe this is an example of dangling by a trivial feature. But it’s so basic a capability that I’m surprised Svelte is on version three without an officially blessed way to do it. I ran into this limitation when I created my second component. Way before I got to try out any of the cool reactivity that earns Svelte all that buzz.
So I changed my mind and went with React. After an hour or so, I’d finished moving everything over — and my anxiety had vanished. I stopped worrying about having most efficient component system, and picked up work on the thing I wanted to build in the first place.
No, React isn’t perfect. It’s optimized for apps that make network requests and then display lists of things (i.e. most apps) which isn’t really what I’m doing here2. The performance is fine now, although I expect I’ll have to optimize as my project gets more complex.
But React lets me stop thinking about the framework. React gets out of my way. React is boring. React is actively developed. React has a giant ecosystem and a giant community. React is battle-tested on some of the most visited websites in the world.
I’m sure there are technically better ways to build a highly interactive interface on the web, but life is too short for me to spend hours trying to figure them out. There are things I want to create, and the only way I can actually create them is to stop spending so much time on tooling.
For now, I’m choosing React.
The solution they seem to have landed on — letting the child expose CSS custom properties as props — is actually pretty cool, though I don’t like that Svelte will silently wrap your component with an extra
Although Dan, if you read this, the “animation pass” mode you’ve mentioned offhandedly sounds very relevant! ↩︎