A female comic book hero bearing a vanilla sigil on her chest

The unreasonable effectiveness of vanilla JS

Joeri Sebrechts

I have a confession to make. At the end of the Plain Vanilla tutorial's Applications page a challenge was posed to the reader: port react.dev's final example Scaling Up with Reducer and Context to vanilla web code. Here's the confession: until today I had never actually ported over that example myself.

That example demonstrates a cornucopia of React's featureset. Richly interactive UI showing a tasks application, making use of a context to lift the task state up, and a reducer that the UI's controls dispatch to. React's DOM-diffing algorithm gets a real workout because each task in the list can be edited independently from and concurrently with the other tasks. It is an intricate and impressive demonstration. Here it is in its interactive glory:

But I lied. That interactive example is actually the vanilla version and it is identical. If you want to verify that it is in fact identical, check out the original React example. And with that out of the way, let's break apart the vanilla code.

Project setup

The React version has these code files that we will need to port:

To make things fun, I chose the same set of files with the same filenames for the vanilla version. Here's index.html:

The only real difference is that it links to index.js and styles.css. The stylesheet was copied verbatim, but for the curious here's a link to styles.css.

Get to the code

index.js is where it starts to get interesting. Compare the React version to the vanilla version:

Bootstrapping is different but also similar. All of the web components are imported first to load them, and then the <tasks-app> component is rendered to the page.

The App.js code also bears more than a striking resemblance:

What I like about the code so far is that it feels React-like. I generally find programming against React's API pleasing, but I don't like the tooling, page weight and overall complexity baggage that it comes with.

Adding context

The broad outline of how to bring a React-like context to a vanilla web application is already explained in the passing data deeply section of the main Plain Vanilla tutorial, so I won't cover that again here. What adds spice in this specific case is that the React context uses a reducer, a function that accepts the old tasks and an action to apply to them, and returns the new tasks to show throughout the application.

Thankfully, the React example's reducer function and initial state were already vanilla JS code, so those come along for the ride unchanged and ultimately the vanilla context is a very straightforward custom element:

The actual context component is very bare bones, as it only needs to store the tasks, emit change events for the other components to subscribe to, and provide a dispatch method for those components to call that will use the reducer function to update the tasks.

Adding tasks

The AddTask component ends up offering more of a challenge. It's a stateful component with event listeners that dispatches to the reducer:

The main wrinkle this adds for the vanilla web component is that the event listener on the button element cannot be put inline with the markup. Luckily the handling of the input is much simplified because we can rely on it keeping its state automatically, a convenience owed to not using a virtual DOM. Thanks to the groundwork in the context component the actual dispatching of the action is easy:

Fascinating to me is that index.js, App.js, TasksContext.js and AddTask.js are all fewer lines of code in the vanilla version than their React counterpart while remaining functionally equivalent.

Hard mode

The TaskList component is where React starts really pulling its weight. The React version is clean and straightforward and juggles a lot of state with a constantly updating task list UI.

This proved to be a real challenge to port. The vanilla version ended up being a lot more verbose because it has to do all the same DOM-reconciliation in explicit logic managed by the update() methods of <task-list> and <task-item>.

Some interesting take-aways:

That wraps up the entire code. You can find the ported example on Github.

Some thoughts

A peculiar result of this porting challenge is that the vanilla version ends up being roughly the same number of lines of code as the React version. The React code is still overall less verbose (all those querySelectors, oy!), but it has its own share of boilerplate that disappears in the vanilla version. This isn't a diss against React, it's more of a compliment to how capable browsers have gotten that vanilla web components can carry us so far.

If I could have waved a magic wand, what would have made the vanilla version simpler?

All in all though, I'm really impressed with vanilla JS. I call it unreasonably effective because it is jarring just how capable the built-in abilities of browsers are, and just how many web developers despite that still default to web frameworks for every new project. Maybe one day...