A modern component pattern for AlpineJS integrated with server side rendering (examples using Django)
2023 has been the year of expressive HTML for me, with a rapidly maturing new breed of front end frameworks hitting the big scene. These frameworks, such as AlpineJS and HTMX, are designed to make building reactive, first class front ends less complex and more elegant than ever. Thousands of devs are deciding to use these frameworks for their next project, and I expect they will represent the future defacto front end of backend-centric web applications. I’m one of those devs, and in this article I’ll describe the patterns I used while building AlpineJS-based features used by millions of users.
Why Alpine?
One of the most critical features of these new frameworks is how smoothly they integrate with traditional server-side web frameworks. Where older heavier front end frameworks like React require their own clunky servers apart from the rest of your backend, AlpineJS and HTMX encourage you to see clientside dynamics as something that exists as a part of your application, instead of your application existing within the frontend framework.
<html>
<head>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>
</body>
</html>
I evaluated both AlpineJS and HTMX earlier this year and preferred AlpineJS. Generally their capabilities are similar, but Alpine felt like it didn’t reinvent the wheel as often. HTMX is often paired with a library from the same developer called Hyperscript that lets you write script within HTML attributes. Alpine does the same thing except with plain Javascript. I decided Alpine will have more staying power being the more standards-oriented framework and thus was the better time investment to learn.
How’s Alpine working out?
So far I’ve made a few major features using Alpine that are deployed to millions of users, and have been pleased with the results. I feel the Alpine-based code I wrote is more succinct and beautiful than with other frameworks I’ve used in the past. Now I’m convinced Alpine is currently the best front end framework for almost all of the needs I have as a Django developer. Thank you Caleb (creator of Alpine) for your gift to the world!
If anything, one of the biggest problems with Alpine is that because part of its implied manifesto is to try to disappear into the HTML, it has an inherent lack of structure that can be daunting to organize. There hasn’t been enough time for established patterns to develop, so I had to invent some for my projects.
Components with AlpineJS
Component Based Architecture (CBA) is a pattern where you create building blocks for your front end that can be assembled like legos. All of the visual structure and all of the dynamics of a feature are implemented in the same place. Everything about a component is detached from the rest of your application so you can reuse it anywhere and as many times as you’d like with as little context as possible. CBA is sort of a visual equivalent to Object-Oriented Programming (OOP). Here’s a good Quora article to learn more about CBA.
Components are a helpful pattern because they reduce inter-connectivity in the codebase, which significantly reduces complexity in the long run, and they also organize code by feature instead of by context, which makes them more convenient to read and understand.
I have been a fan of components since I started using them in React, and was excited to see that Caleb has been hard at work pioneering how to create components in Alpine with his paid resource. I felt like there was more room on the table to optimize the direction he was taking so I went to work.
A simple Alpine button component
Here’s a button component implemented as a Django template button.html, to discuss:
{# An AlpineJS button component #}
<template id="buttonPrototype">
<button x-text="displayText" @click="doSomething"></button>
</template>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("button", () => ({
displayText: "",
init() {
this.displayText = this.$el.innerHTML;
this.$el.innerHTML = document.getElementById("buttonPrototype").innerHTML;
},
doSomething() {
this.displayText = "Clicked";
},
}));
});
</script>
And the use of the button in another template, index.html:
{# A test site index #}
<html>
<head>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="button">Welcome to Alpine Components!</div>
<div x-data="button">(Another button)</div>
{% include "components/button.html" %}
</body>
</html>
Here’s a JSFiddle of the example code to play with.
Lets step through what’s happening.
- Index.html is rendered server side with button.html pasted in at the bottom of the body. The resulting HTML is sent to the client.
- The client downloads the HTML and renders the basic DOM (including a div with Welcome to Alpine Components!)
- AlpineJS is downloaded and interpreted
- When Alpine is loaded, it calls the alpine:init event
- We respond to the event by calling Alpine.data(…) and register an Alpine data component called button.
- When the button data component is registered, Alpine goes through the DOM and looks for x-data attributes set to button. It finds one and proceeds to create an Alpine component and paste in our definition registered to button.
- Our button component has a magic function called init. Init is called automatically upon creating an alpine component as if it were an x-init attribute on the same element that has the x-data.
- Our init gets the innerHTML of the DOM element that x-data=”button” is on and sets its alpine state variable displayText.
- Next our init finds the template DOM element defining the button’s HTML and copies its HTML into the x-data=”button” element.
- The DOM resolves to something like this:
<div x-data="button">
<button x-text="displayText" @click="doSomething">
</button>
</div>
11. Now Alpine registers the attributes on the button tag, and once x-text is registered, Alpine updates the innerHTML of the button tag to the current state of displayText.
<div x-data="button">
<button x-text="displayText" @click="doSomething">
Welcome to Alpine Components!
</button>
</div>
12. If the button is clicked, it will call doSomething
Extra considerations
We’ve found the pattern above to scale well, but I have a couple notes.
One of the more complex parts of component architecture is disseminating initialization data to components. This is typically done through a “props” object in other frameworks, but since the initialization vector of Alpine is within HTML, it needs to be a bit different. We found the strongest patterns are to include the props as:
- The innerHTML as the example above shows
<div x-data="button">Welcome to Alpine Components!</div>
2. Data attributes on the usage tag
<div x-data="button" data-displaytext="Welcome to Alpine Components!"></div>
3. Serialized data (possibly JSON) passed to the x-data function
<div x-data="button('Welcome to Alpine Components!')"></div>
Alpine.data("button", (displayText) => ({
...
Another initialization pattern that devs using Alpine and HTMX are experimenting with is rendering components server-side and having their initialization vector be directly from the the server side template into the javascript for each instance of a component. On some more extreme examples, devs are calling a separate GET request for each individual component instance on a page. I feel this is a weaker pattern because it adds additional locations for related code to exist (new views and URL routers) and has more moving parts that could break. Not to mention it has much higher latency and resource usage.
As another consideration for the component pattern I described above, smashing a bunch of different languages together into one file can get complex to implement code quality automation tools for. For this reason, it might make sense to separate the HTML and JS into their own template files:
{# button.html #}
<template id="buttonPrototype">
<button x-text="displayText" @click="doSomething"></button>
</template>
<script>
{% include "components/button.js" %}
</script>
{# button.js #}
document.addEventListener("alpine:init", () => {
Alpine.data("button", () => ({
displayText: "",
init() {
this.displayText = this.$el.innerHTML;
this.$el.innerHTML = document.getElementById("buttonPrototype").innerHTML;
},
doSomething() {
this.displayText = "Clicked";
},
}));
});
We strongly believe in packaging the alpine component JS into the server-rendered HTML to reduce browser render latency, but if its desired, separating the JS in this way would also be a good way to use it with webpack or similar.
There’s lots of ways to skin an Alpine cat
I’m really enjoying using the pattern above, but since Alpine is so flexible, I’m looking forward to the other patterns the community comes up with. Perhaps there will be extensions for Alpine eventually that attempt to formalize component structure. We’ll see! Let me know what you think of my take!