Clean Web Apps Without an Opinionated JavaScript Framework


Note that this was originally posted for a tweet while I was still relatively early in our development stage. I’ve single-handedly shipped an app to production and this has worked great, but time will tell if it holds up as our development team and application size grows. I’ll be coming back to this article to update it as we solve pieces.

That said, I’m not pioneering anything. These tools are already used by many applications you use every day.

Jumping to a new framework may be the correct answer in some cases, but I’ve seen a lot of these posts recently and wanted to give another option that I use. Keep in mind that I’ve not done much in React, Vue, Nuxt, Next, etc. I’m a backend developer brought into the world of front-end by necessity. I love designing UIs and improving UX flows and I wish all engineers got the chance to be full-stack.

I can’t (yet) share a major success story about our product’s scalability, speed, etc., but I can share details of how we achieve certain high-level needs in our web apps. For success stories to pique your interest, here’s what OpenUnited said about their choice to switch from React to a more Vanilla codebase.

Our “deliberately simple” frontend means that we use Jinja templates, TailwindCSSTailwindUIHyperscript, plain javascript where needed, and HTMX where it improves the UX. Earlier we had a separate ReactJS frontend and a GraphQL API layer, however such fanciness failed to deliver the expected value, whilst creating complexity/friction… therefore, we now have a deliberately simple frontend. As a result, we have about 50% less code and move way faster.

OpenUnited: https://github.com/OpenUnited/platform

For another well-rounded UI example, see Contexte. Here’s some handpicked pieces from the HTMX post about them switching to HTMX from React:

  • No reduction in the application’s user experience (UX)
  • They reduced the code base size by 67% (21,500 LOC to 7200 LOC)
  • They reduced their total JS dependencies by 96% (255 to 9)
  • They reduced their web build time by 88% (40 seconds to 5)
  • First load time-to-interactive was reduced by 50-60% (from 2 to 6 seconds to 1 to 2 seconds)
  • Web application memory usage was reduced by 46% (75MB to 45MB)
A Real World React -> htmx Port: https://htmx.org/essays/a-real-world-react-to-htmx-port/

Clearly, the “Vanilla Framework” doesn’t have to be a monster. You can choose the simplicity of not having an opinionated JS framework and still have a clean codebase. You can even do it without a build step if you so choose.

Components

The core issue that is easy to run into is that there are no components by default so you feel like you have to duplicate code, styles, etc.

So how do you get components? Many backend frameworks already provide a rich templating language for reducing your HTML into sub-components. Learn at least the basics of passing variables into them and how to do formatting. Also, try to make them more generic. If you’re creating a form, e.g., you should only have to create the form template once per project.

I predominantly use Django, so a nice ancillary tool is the django-components package which allows creation of a single component that can tie CSS, HTML, and JavaScript into one modular piece. It will even smartly insert the rendered output into an HTML document the way you would normally write it if it was a one-off page. The CSS gets put in the <head> tag and JavaScript gets combined into <script> tags at the end of the body.

This means that if you want to quickly build up a new page, you’ll already have a button component, dropdown picker, etc. from the rest of your site. It’s a simple line something like:

{% component "button" button_text="Submit" ... %}

Buttons are easy, but they’re foundational. What if we want an input with a button like an email signup form? We can just make a new component like by combining the two base pieces.

{% component "input" placeholder="john@doe.com" ... %}
{% component "button" button_text="Submit" ... %}

This encapsulation of JavaScript, CSS, and HTML works great when rendered into a template, but as we’ll get into later, it has some drawbacks when used with other tools like HTMX. As a result, I don’t often use it to bundle the three parts to a component.

The real magic of django-components is the slot tag. Rather than trying to embed all the arguments in a single line like you would end up doing with a standard template, you can easily separate them with {% slot %} and {% fill %} respectively.

Readability matters and this helps a lot.

{% component_block "my-component" %}

  {% fill "title" %}
    My Title
  {% endfill %}

  {% fill "content" %}
    Once upon a time...
    You could write multi-line blocks.
    <span class="font-bold">And use HTML tags</span>
  {% endfill %}

{% endcomponent_block %}

Updates

You need a way to update individual components when data changes. This is what HTMX was designed for. See the examples here and you may be hooked. I may write more about this someday, but the HTMX site gives an excellent start.

At it’s core HTMX let’s you in a few characters (think syntax like hx-get="/endpoint" in your HTML) issue an AJAX request to your server and swap out targeted content on a page. The rest of the very small library handles nearly every real life case where this would seem to have issues.

Unfortunately, selectively updating parts of a page means that the django components package can no longer insert the necessary JavaScript and CSS into the “proper” place in the initial template rendering where the components are processed. Everything is dynamic, and it has no way to “guess” which components will be needed.

This is why I prefer to bundle all of the pieces for a component into one file. If you use Tailwind, the CSS part is already covered. Your classes are for the most part already in the HTML.

Side note: I pushed back on Tailwind for a while, but it’s improved my designs significantly just from the consistency baked into the package. I’m not a fan of complex build steps, but I’m happy to make exceptions where it’s beneficial and I think this is one of those cases.

For JavaScript, my general pattern is to embed it in the root tag of the component and then reference anything in the component by it’s parent. Putting it all together, it looks something like this:

<div class="[TAILWIND CLASSES]">
  <!-- ... -->
  <script type="text/javascript">
    let root = document.currentScript.parentElement;
    // ...
  </script>
</div>

I can now render however many of the component I’d like (even dynamically with HTMX), and the JS/CSS will be consistent for all of them.

Events

Part of a good UI is keeping everything up-to-date. When you add an item in a popup modal form, you expect the table that lists all your items to also update to how the new item. With HTMX, updating arbitrary numbers of other components in the UI when a value changes is done using events. It’s simple, but powerful. When you return a response, add an HX-Trigger response header. That’s as simple as doing this in your view function:

response = ...
response["HX-Trigger"] = "customerAdded"
return response

Then when HTMX handles the response and on the client side, it will issue an event “customerAdded”. Anything listening to that event can then trigger an update to it’s own respective component. You can trigger however many events you need by comma-separating them.

Summary

We haven’t slain all the monsters yet, but some people have to great effect, and it’s worked marvelously for us up until to this point. To uphold KISS, I’m sticking to this design until I have reason to believe otherwise. It is possible to adhere to good patterns without a framework and is not necessarily more difficult to build reasonably complex applications.