DEV Community 👩‍💻👨‍💻

Edwin
Edwin

Posted on • Updated on

Building a TODO app without a bundler

Do you remember the time before front-end frameworks and build tools, where you would sprinkle some JavaScript on top of your HTML to create interactivity? Code up your HTML documents, preview them in the browser without tools like Webpack, then push them to your webserver using FTP?
I sure do. 👴

What if I told you, you can build modern web apps and have still have a smooth development workflow without any build tools?

In this article I'm going to implement the TodoMVC app without any build tools and only use native JS functions supported by evergreen browsers (sorry Internet Explorer, it's time for you to leave).

I will use some libraries related to React, but you could write the app using anything you prefer (or no library at all). What matters most is the fact that we simplify our development process by cutting out tools that are required to work with these modern day frameworks. The starting point is just a HTML document with a <script> that initializes our app, while SPAs often start from an index.js entry point and try to control the document from there.

Here's the source code and the end result:

Single Page Apps

When building an interactive web app, developers usually reach for frameworks such as React, Angular, Vue, Svelte, to name a few. These frameworks are mostly abstractions and best practises to help you create modular code while staying productive. They all come with a set of supporting tools to smooth out the development process: translate modern JavaScript features to something all target browsers understand, manage dependencies, optimize the output code, etc.

These interactive client side apps are often Single-Page Applications: a web application that loads a single document and then updates the page content using JavaScript by loading other modules and fetching data from a REST API.

Not every website needs to be a SPA, mind you. In fact, the approach below could be used in a good old multi-page website, where you sprinkle the JS on top of the page to create the interactive ToDo functionality.

Goals

We are going to build a simple TODO application such as this one, which is fully client side and has a clear scope.

  • Implement the TodoMVC app using this specification.
  • Use only native ES6 browser features.
  • No build tools (Babel, Webpack, etc).
  • We still want to be able to use NPM packages.
  • Supports latest stable version of Chrome, Firefox, Safari, Edge.

Why would you go buildless?

Let's start with the main reasons we still need bundlers in 2022:

  • The NPM ecosystem is built around packages being able to run in NodeJS, not primarily for the web. NPM packages are expected to use the CommonJS format to ensure everything is compatible with each other. Publishing a package using pure ES Modules would break that compatibility. Seems backwards, right?
  • Packages use a short-cut method of importing other packages by their package name, without an extension (bare imports), e.g.: import groupBy from lodash/groupBy instead of import groupBy from './node_modules/lodash/groupBy.js. Tooling is needed to fix the module resolution.
  • Bundlers take care of a lot of implicit stuff, like polyfilling missing features. A lot of NPM packages expect this stuff to just work.

Pika is doing an awesome job at rethinking this whole process and it questions why we need web bundlers today at all. Check out this great talk:

The reason to ditch all this tooling seems obvious: it simplifies development, because you only need to deal with native JavaScript. No tools to learn, no configurations to manage, no more waiting for your app to start up.

You get some additional benefits too:

  • Your development environment is exactly the same as your production environment, which can make debugging easier.
  • No security risk of installing third party code during development. NPM packages can basically run any code on your machine using post-install scripts.
  • Modules are cached individually. Updating a single module means other modules stay cached. This is more of a hassle when using Webpack.

Downsides of going buildless

  • No pre-processing, so you lose access to tools like TypeScript, LESS/SASS (for CSS).
  • No minification or tree-shaking of application code.
  • Slight performance hit compared to loading bundled JS. Large JS files still compress better than smaller individual files. So there is some benefit in bundling all code into a single file. HTTP/2 might resolve some of that issue, but I haven't seen concrete numbers yet. See also this discussion.
  • Managing module imports can become messy, resulting in long relative import paths ../../../module/subModule/component.mjs. Webpack has aliases to make your life easier. JS import maps can fix this natively, but they are not supported by all mainstream browsers yet.

You win some, you lose some.

Using third party libraries in a buildless setup

Just because we aren't allowed to use build tools, does not mean we can't use any NPM libraries. To load them, we have several options.

Content Delivery Networks (CDNs) are free online services that serve NPM packages over the network. Examples are jsDelivr, unpkg and SkyPack. We will be using these services to import the libraries we want to use.

You can import those packages using a script tag, for example:

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

ES modules allow you to import directly from a URL:

import groupBy from 'https://unpkg.com/lodash-es@3.10.1/collection/groupBy.js';
Enter fullscreen mode Exit fullscreen mode

Learn more about ES imports in this article

Libraries for the buildless route

We are looking for libraries that use ES modules, so we can plop them into our app and use them like any other utility function.

  • Lit Element which builds on top of the web components standard. (example app)
  • Vue Single File Component loader allows you to sprinkle Vue on top of any HTML document. (example app)
  • HTM - a library that lets you write components using JSX-like syntax using template string.
  • Symbiote - framework that allows you to write class based Custom Elements, focused on building complex widgets that you can then embed in other apps.

HTM, Preact & JSX

I feel very productive writing front-end UI components in React using JSX, so I wanted to have something similar for this app. After some googling I stumbled upon HTM, which promises JSX-like syntax without bundling, so I decided to give that a try. HTM plays nicely with Preact (a leaner version of React with only slight differences).

Coming from React, the biggest difference is the way you write the JSX:

// React
const root = createRoot(container);
const MyComponent = (props) => <div {...props} className="bar">{foo}</div>;
root.render(<MyComponent />);

// HTM + Preact
const MyComponent = (props, state) => htm`<div ...${props} class=bar>${foo}</div>`;
render(htm`<${MyComponent} />`, container);
Enter fullscreen mode Exit fullscreen mode

State management using Valtio

Valtio uses JavaScript proxies to wrap your state objects and track changes automagically. ✨

The state can be manipulated outside the React/Preact lifecycle too using vanilla JS. Persisting the app state to localStorage is also trivial.

The library is light-weight and easy to work with. Valtio is certainly not required for the no-build app, but it felt like a good match for this setup.

Implementing the TODO app

I would like to use a component based development approach without writing everything from scratch, so I decided to use HTM with Preact. This allows me to write JSX-like syntax without a transpiler.

I won't dive too deep into the implementation itself, but you can find the source on GitHub.

Getting started

Create an index.html file and add a <script> tag and point it to js/index.mjs - the starting point of the app:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>No-build ToDo app</title>
    </head>

    <body>
        <script type="module" src="js/index.mjs"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

We can import the CSS for our TODO app directly from a CDN like so:

<link rel="stylesheet" href="https://unpkg.com/todomvc-common@1.0.5/base.css" />
Enter fullscreen mode Exit fullscreen mode

In the index.mjs file we can import() any other modules that we need. From here we can start writing modular components as we would do when using React!

// js/index.mjs
import { html, render } from './modules.mjs';

import { Header } from './Header/index.mjs';
import { Footer } from './Footer/index.mjs';

const App = () => {
    return html`
        <${Header} />
        <section class="todoapp">
            <!-- etc -->
        </section>
        <${Footer} />
    `;
};

render(html` <${App} />`, document.body);
Enter fullscreen mode Exit fullscreen mode

Beware that we need to write the full path, including extension, when importing a module - this is how ESM works.

All our third party modules are defined in js/modules.mjs, which I'll explain next.

ReverseHTTP CDN

I'm using ReverseHTTP as a CDN because it can produce an optimized bundle of packages in a single HTTP request. You only need to put a comma separated list of modules in the URL and the service sends an optimized, cached bundle back. It's like having your node_modules folder in the cloud.

Here you can see what is inside the bundle that I'm using for the app:

https://npm.reversehttp.com/#preact,preact/hooks,react:preact/compat,htm/preact,uuid,valtio/vanilla,proxy-compare

Image description

It weighs 14,49KB with Brotli compression (35KB uncompressed).

To keep things a bit maintainable, I import the bundle once in modules.mjs and then re-export everything, so my I can reference to a centralized point in my own code.

// js/modules.mjs
export * from 'https://npm.reversehttp.com/\
htm/preact,\
preact,\
preact/hooks,\
proxy-compare,\
react:preact/compat,\
uuid,\
valtio/vanilla\
';
Enter fullscreen mode Exit fullscreen mode

Then I can just import anything from this modules file:

import { html, useState } from 'js/modules.mjs';
Enter fullscreen mode Exit fullscreen mode

This stuff is pretty wild. 😅

Run the app

Now we only need to some kind of static file server so we can preview the app in our browser. You can use the VSCode Live Preview extension or use a simple static server like this:

npx serve
Enter fullscreen mode Exit fullscreen mode

When using the Chrome developer tools, you can see in the network tab that imported modules are loaded individually:

Chrome Network tab in Dev Tools

Conclusion

Creating an app without a bundler was a fun and overall a pretty smooth experience. ES6 has all the language features needed to create apps with a great developer experience. We've seen how dependencies can be imported from a CDN to add third party code to our app without the need for extra tools.

Still, I probably wouldn't go without a bundler for production apps in 2022. Choosing which tools to use is a trade-off between complexity of the build process and productivity + optimizations that you get by using these tools.

Pika is a great initiative that moves the complexity of build tools away from the app. It is a step towards a simpler development process. It's nice to see that the JS ecosystem is moving towards ES modules, which makes a lot of sense to me.

Sources

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.