Designing The Lustre Web Framework (with Hayleigh Thompson)

Source

Web app architecture is still an evolving field. I'd say the front-end user experience feels like it's stabilized. We all know roughly what a web UI is supposed to look and feel like. However, how it's architected under the hood is definitely still in flux. Should we be using REST, WebSockets, or GraphQL?

How much rendering should you do in the browser, and how much should be handled by the server? How do you manage the state of a web page when, these days, a web page's state can be so large that it definitely requires some management? Modern web pages are complicated entities, and we need good architectural practices to handle them. Joining me to discuss some possible next steps in the evolution of web app architecture is Hayley Thompson. She's a React developer and an Elm developer, two quite different frameworks in two very different languages.

Synthesizing her expertise into a new framework called Lustre, which incorporates some inspiration from the actor model. Lustre has connections to Erlang, and there are parts of Lustre that resemble location-agnostic actors more than traditional web pages. How does that work, and why is it worth pursuing? What are the advantages?

The upside is all in pursuit of the perfect architecture. So let's see where she's taking this. I'm your host, Chris Jenkins. This is Developer Voices, and today's voice is Hayley Thompson. My guest this week is Hayley Thompson.

Hayley, how are you? Yeah, I'm really good, thank you. How are you? I'm very well, very well. You've brought up one of my favorite topics, which only became my favorite topic about halfway through my career. We can talk about it! I got into front-end programming probably around the early 2010s, just out of sheer frustration with the quality of engineering, in my mind and in my experience, where you weren't supposed to complain about anything else if it worked.

And I found Angular 1, which was, in hindsight, not the greatest design, but at least there was a design. That's when I really started getting into not just front-end programming, but also how we can make the engineering of it sane. That was my starting point, but it's about you. So what's your starting point in the front-end world? Well, actually, my background is in music computing and creative coding.

So I actually got into web development through the web audio API. Yeah, like, I was teaching myself programming, and the idea of learning C was kind of off the table. It was a bit too complicated for me.

So when I discovered that JavaScript had this API for synthesis in the browser, I kind of just ran with that. I probably started around 2016. Back then, I was doing a lot of C++ in OpenFrameworks and Java with Processing, focusing on creative visual coding. Okay, Processing is an audio framework in Java, right?

It's more of a visual framework, actually. There's an audio module, but at the time I was working on generative art and concurrently learning web development because it sounded interesting. Then I discovered the Web Audio API and started playing with that. I picked up Vue maybe in 2018.

I was doing that for a while. I leapfrogged over React and TypeScript and all of that. At the time, npm, Node, and all of this were super intimidating to me. I really liked that I could just drop Vue in a script tag and work with it. I didn't really understand package management or anything like that at the time.

Right. And then in 2019, I came across Elm. Yes, I took a functional programming class at university that taught me Haskell.

And I had seen Elm previously, maybe a year before, and bounced immediately off it because it looked super weird; it was very strange. After I did this cast, I kind of came back to Elm. There was this big push at the time, like maybe in 2019, and a lot of the big names in the Elm community were giving many conference talks.

I was getting a lot of recommendations on YouTube about how Elm was solving all of these big problems, like Elm being similar to TypeScript, React, Redux, Immutable.js, and Webpack, all in one package. So, I just picked it up, and the rest is kind of history. I've been using Elm now for five years. For about a year or so now, I've been developing in my mind what Elm 2 would look like in Gleam.

Right, which is luster, right? Exactly. So we're going to get into luster, but Elm 2 in Gleam, right? Which is luster, right? Exactly.

Okay, so we're going to get into Lustre, but the fact that you've created Lustre positions you as an architect of front-end design patterns. So, I'll give you the interview question: how would you describe and contrast the way Vue does it to the way Elm does it? Yes, that is a very good interview question. Sorry, this is an interview, but not that kind of interview, I think. What a lot of current front-end frameworks come down to is this very component-driven architecture. In many ways, you're thinking on quite a small scale. You're encouraged to focus on individual components. Vue is a really good example, actually, because they have these single file components (SFCs), as they call them. You have the markup for your component, then a script tag encapsulating all of the logic, and finally, a style tag for styling the component. The idea is that by focusing on the component level, you can create rich, interactive, sophisticated experiences and then compose them into something much larger. This stands in stark contrast to Elm, where you have a very holistic view of your application.

Okay, what are the main domain types in the application line building, for example? What does my model represent? Your model represents your entire application state. From there, your UI consists of pure functions. Right now, you have no component state or local component state.

Instead, you have just simple building blocks to piece together your UI, which is largely divorced from your application logic and your business logic. I think these two approaches are very, very different. It is that dividing line almost between object-oriented and functional programming, right? In one, you think of the world as broken up into little islands of objects, and in the other, you think of it as data and the actions you perform on that data.

It's interesting because in the Elm guide, there's actually a footnote written by Evan that acknowledges that a lot of people ask about where components are in Elm. So, there's this footnote that explains that components are objects, and we don't have objects in functional programming, and so forth. How did you find the transition from one model, which is probably a more popular object-oriented model, to the functional programming model? Actually, the transition was, in some ways, predictably difficult because it is a very big shake-up of your worldview, I would say. However, I was early enough in my journey in general, and the scale of the things I was building and working on was small enough that the transition to this kind of Elm approach, where you have a model that describes your whole application, worked really well for me.

And I actually found that transition very smooth in a lot of ways, and it now feels very, very natural. Okay, okay. It sounds like you found it easier than I did, but I was more baked in the whole "objects are everything" worldview. Yeah. Okay, so I think these things are less stark.

Different at small scales, as you say you're dealing with small applications. Yes, they're different, but the differences don't really show themselves until things get large. Have you worked on large Elm projects and large Vue projects? I haven't worked on large Vue projects, but I am also a React developer these days. Okay, okay.

So, I have worked in both large Elm and React codebases lately. Do you want me to compare and contrast them without being too biased? Actually, I don't think I am that biased; I'm a really big fan of React. After spending four years working with Elm before I even looked at React, I found that when I finally came to React, it offered me many features that I wished I had in Elm.

Oh, okay. So I think both approaches can scale, I would say. In my experience, I've worked in large codebases in both, and both of them have been pleasant enough. I think they both have their own pain points. However, I believe there's a significant shortcoming in Elm's model, which is its complete rejection of encapsulated state. This, I would say, is a very dogmatic approach to encapsulated states as objects. The notion that objects are OOP and OOP is bad breaks down very quickly when you consider the DOM and the HTML that you're already rendering.

Explain that to me. The example I always give is the input element, the native HTML input element. This element has so much state that you don't ever interpret it, right? The native HTML input element has so much state that you don't ever interact with it, right?

Like we set the value of it. But think about managing focus, like if you tab into it and now it's focused. This is an internal state. Or think about handling keyboard interaction, or even something like blinking—exactly the placement of the cursor, like the cursor blinking when you make a selection. All of this stuff is handled by the browser, but it's managed as an encapsulated, stateful thing. In our maps, we say, "Well, we don't worry about that. We're going to render the marker." I think there are times, especially now in modern web development, where we're really crafting quite sophisticated applications, right?

We've moved beyond documents, or at least the industry that I'm in has moved beyond documents into applications. Here, you have very rich interactive elements on a page. If you really want to craft a native experience, I think it very quickly falls down in Elm's model, where you have to store all of that somewhere centrally. Yes, okay.

I can buy one place I've experienced that is trying to do masterminds centrally. Yes, okay. I can buy one place I've experienced that is trying to do maps in Elm, and they are sophisticated. Efficiently rich, that it does start to become very difficult.

But, okay, so you're not supplying enough controversy, so I'll do it, right? The polarizing line, and all programmers try to be polarizing at some point, is that Elm tries to reject the existence of state except for one big global state. I think that works quite well generally, highlighting some problems. On the other end, where every tiny little thing becomes a component, I find the problem there is that every individual unit works well, but you've got this hidden graph of interactions, which becomes impossible to debug.

If you're saying components are sometimes good, I think that's what you're saying. How do we know where to divide up those lines? I think a lot of it comes down to culture. I mean, I'm going to keep hammering on about Elm because it's the thing I'm most familiar with and also the thing I have the most opinions about. Okay.

And so, as a culture, as Elm developers, we have this rejection of encapsulated state. However, if you look at large Elm codebases or complicated packages, there is this implicit recognition of, "Well, I have some behavior that I want encapsulated, so I'm going to put it in an opaque type. I'm going to tell you to store it in your model, and I'm going to give you some functions to update it and a function to render it." What we've done there is acknowledged that this is sufficiently complicated that I want to hide how it works from my users, from my consumers. However, Elm is not expressive enough to achieve this in a way that doesn't require breaking it into three isolated pieces and asking your users to wire things together. As Elm developers, I think we have a pretty good intuition of when we should do that.

Because the language and the framework itself make it quite tedious, it's not an approach we resort to immediately. In contrast, with React, Vue, Angular, or Svelte, it's very easy to create a component with encapsulated state. The culture there emphasizes creating many encapsulated components. It's quite obvious to say, but it ultimately comes down to what the language makes easy. If you make something hard, your users will only do it when they absolutely need to.

I'm going to play devil's advocate for the Elm team here and say the reason it makes creating components difficult is that they're a bad idea. But I would say that it's clear. Would that be the fair Elm line? I think it's the fair Elm line. However, if you take just half an hour to look at things being built with Elm, you will see that they are still creating components.

They're just doing it in a way that is technically not a component but is functionally identical to one. If you publish a package or if you're working internally that is technically not a component but is functionally identical to one, right? If you publish a package or if you're working internally, if you have a module that has its own model type, a message type, an update function, and a view function, you have, in essence, created a component. However, you can't put those pieces together yourself, so you hand the constituent parts to your users and say, "Right, wire this up." But it's a black box as far as you're concerned. Yeah, yeah.

Okay, yeah. I'm now thinking of a table library I've used in Elm. It is not as customizable as I'd like, but it's great when you just want to set up a table with sortable columns. I always have to double-check and triple-check how to install it because I'm installing it in three different places.

Exactly. The ARM community has a significant infatuation with web components, which is somewhat uncommon among other front-end frameworks. I think, as a front-end community, we've largely landed on the idea that web components aren't that good anymore. However, the Elm community, in particular, is very bullish on the idea that when you have encapsulated state, it's best to put them in a web component and render them in your view. I've seen people experiment with building maps as web components, which indicates that there are individuals who recognize the value of packaging sophisticated logic combined with Vue into a "black box."

Because this web component is very much like the input tag, right? Exactly. You treat it like HTML, but it actually hides a whole bunch of interesting state. I can see why, philosophically, Elm people would be attracted to that idea, as they're already using it in even simple form fields. However, you seem to think it's not such a great idea?

I think it’s an excellent idea. Oh, okay, I've misinterpreted you. Yeah, I'm a big fan of that idea, and in fact, this is how components work in Lustre. Just to give a quick overview, Lustre follows a similar model-view-update concept as Elm. You have a model of your application state, and a message type that represents all the ways the world can communicate with you.

You update that state, and you have a kind of pure view function. Born out of this frustration with Elm, what you can do in Lustre is take those building blocks from the model, view, and update package and say, "Okay, treat this as a web component." It registers as a real web component, which you then render in your application as any normal HTML element. You communicate with it in the same way that you would with native HTML.

So you set attributes, and the component sends you events. There isn't this split idea of rendering HTML one way and passing data into HTML another way. Instead, my framework has components, and I have a props object that I pass in. It's quite common these days for the notion of a component to be a framework-specific concept that is detached from the idea of HTML. In Lustre, these two aspects are operationally equivalent at the point of HTML and, at the point of consumption, they are also operationally equivalent.

Does that bring back the problem of, once you've decided, because there is a nice distinction between separating behavior from HTML? The obvious benefit is that you can render it in multiple ways, allowing the same underlying logic to be used for different presentation types. It seems like you might lose that flexibility as you tightly couple the HTML tag to its functionality. I believe it will always come down to API design, and that is unavoidable.

So I would say that, yes, if you have committed to treating something as a component, then you have lost the ability to treat it as constituent parts. But if that functionality was valuable to you, you didn't have to make it a component, right? Yeah, okay. I mentioned previously that it's about culture and it's about what the framework makes easy. There is a bit of overhead to creating a component in Lustre because you do need to build a full application, right?

You do need that model type, you do need that init function, and that message type, right? You need all of those bits. Only once you have those bits can you make them into a component, which is quite different from conventional front-end frameworks. In those frameworks, you might initially start off with a function that just returns some markup, and then, when you decide you actually need state, it's very easy to add state. It’s very easy to turn it into a component.

But the fact that Lustre kind of adds a not insurmountable but obvious hurdle means you're discouraged from making this approach the default. The default approach is still always to write pure view functions. Okay, so you're saying if I come into Lustre, I will get largely the Elm experience, which is to write some data describing my idea of the world and how it can change, and then write some rendering code. However, the option to turn it into a web component, which is like a live HTML tag—a smart HTML tag—is very much open to me.

Yes, that's pretty much the ideal scenario. Okay, is that novel? I mean, I've never actually tried in earnest to create web components in other frameworks.

Is that common? Is it well supported? Do you think you do it better than anyone else? Yeah, I think the best example I can think of is Svelte.

You can compile any Svelte component into a web component, at which point Svelte has disappeared, and you can use it anywhere. This is the same in Lustre. I don't think that idea is novel. The novelty of Lustre, however, lies in our concept of universal components. Oh yes, I want to get into that, but first, we need to take a quick sojourn into Gleam and what it offers to build that up, right?

So, lust is written in Gleam, which is a language I know you love. Why Gleam, and what does it offer as a front-end developer first? Yeah, I actually think that's a great question, and I believe my answer may be somewhat dissatisfying.

I think there are a lot of exciting and mature things available. As a front-end developer, you are spoiled for choice. In my opinion, the strength of Gleam is that if you're a back-end developer, you gain access to the front end, which is less common. You have access to the front end that you prefer through Beam's JavaScript target.

So, for people who don't know, Gleam compiles to both Erlang and runs on the BEAM, the Erlang virtual machine, and it also compiles to JavaScript. While it's not exactly novel to have a full-stack language, I do think that if you are a front-end developer, you are spoiled for choice. However, there are reasons to choose Gleam. I would say that the main reason is...

The big one for me is that Gleam is just much semantically simpler than TypeScript. The experience of using it has much lower complexity. There's a lower cognitive burden, I would say. But actually, I think the superpower is for back-end developers who don't really care about front-end development but need a front end because everything requires one now. I think the selling point, then, is that you gain access to a front-end framework created by an enthusiastic front-end developer who, hopefully, knows what they are talking about.

And you get to use the same language, the same concepts, and the same primitives. So, is Luster mostly about bringing good front-end ideas into this world of people who don't necessarily have access to many front-end ideas? That's definitely been my strategy for now. Interestingly, front-end people are a naturally curious bunch, I think. If you're into front-end development, you have almost definitely explored a dozen compiled-to-JavaScript languages and tried 100 different frameworks.

And so, actually, the number of people using Lustre right now, especially, say, four months ago when Gleam had only just become version one, was primarily front-end developers. There’s already a community of knowledgeable people. My focus, as a kind of brand strategy, if you want to call it that, is really to target developers on the Beam. This includes not just back-end developers but specifically Erlang and Elixir developers. I believe this is a viable, interesting, and exciting strategy because I like Elm’s idea of how the front end should work. The Model-View-Update architecture is strikingly similar to what Erlang and Elixir developers are already doing on the Beam with actors. Yes, there is a huge parallel there, isn't there? Because it truly reflects that connection.

There's a huge parallel there, isn't there? Because it's like—yes. It's all about treating the world as a state machine. Exactly, it's entirely message-based functional updates of state. It's just the Elm and the Luster thing; we just add a view function to that building block that you already have—just add a pretty way to look at your actor. I've mentioned in talks I've given in the past that I frame it as your front end being another actor, but not an actor in the BEAM.

Now this actor lives in the browser, but it's the same. You still send messages to it. It still has an update loop. For a lot of those developers, that's a real light bulb moment because it's like, "Hey, this kind of approach to building applications that I'm already familiar with; I don't need to learn a new idea to build the front end." I still need to learn how to put HTML together, but in terms of creating an application that has behavior, you're already doing what you need to do. Yes, and it's interesting because that's exactly the flip side of the coin from where I started out, where people knew how to generate pretty-looking HTML, but there was no fundamental underlying architectural pattern to make it sane.

You're turning that on its head and saying, "Here's the architectural pattern, and now what we need to do is make it look good," which I always outsourced to a designer by getting Photoshop documents anyway. So, you're just left having to center a div, but you know you can make that happen eventually. Well, actually, I have a companion library for that. I have a UI library that is, at the moment, a bit in flux, but the idea is that it's just a UI kit. You don't know or care about front-end development; you just want something consistent and pretty. Right? Is that something like Bootstrap or Tailwind or something conceptually similar?

Yeah, I suppose so. Conceptually, it is similar to Bootstrap in the sense that if you just need something that looks good and a way to put it all together, then it's perfect. Yes, it's absolutely great for prototyping and higher design.

As if anyone complains. Yeah, pretty much. Okay, so I can certainly see that sell, especially for people who are on a backend like Beam, for which there is no obvious frontend framework to go to, as far as I'm aware. The major architectural difference is that if I’m trying to think like an Erlang Beam backend Glean developer, I’m expecting to have lots of different actors running my application. It seems like the frontend is going to largely be a single actor.

Is this where the idea of making web components emerges? How does that play out, the proliferation of actors for tasks? I mean, that is largely it. So, you would expect to have many actors in your application for sure. In most cases, your front end, your view, will be one actor, right? The browser. But I think the appeal of the Lust approach to components is that if you do need more, you're still proliferating that same idea—that same update loop idea—into components now.

And they do compose. So, like in your Erlang applications, your actors form a graph, right? You can have supervision trees, but in terms of communication, you essentially have a graph of actors that can send messages to one another. The only difference with the front end is that, rather than a graph, you have a tree, right? Because you're ultimately rendering HTML.

So there is, but the building blocks are the same. I'm going to bring it up again because I think the most exciting part is that, well, if you have this application type, which just bundles up the idea of an actor with a view function, you can use it as your application, and you can also embed or nest them as components. This idea is already quite familiar to the Beam; why can't you just move that whole bundle as an actor on the Beam? This is exactly what Lustre allows you to do.

Take me through that. Yes, we need to dive into that. So, you have an actor running on the front end generating HTML from a certain point of view. You want to move that to the back end. Why?

I think the most compelling argument is if you need or want direct access to some sort of back-end service, right? Like maybe you're doing real-time data visualization or you have, to me, having a real-time aspect is the real draw to this ability. But also, this ability unlocks the fact that you just don't have to worry about network transport anymore. You don't have to craft an API layer on top of your database, then do some JSON serialization, deserialize it, and then turn it into HTML. You can run your component next to your database, conceptually, and just make database queries and then turn it into HTML.

Right? Run your component next to your database, conceptually, and just make database queries, and then turn it into HTML. Before we get too deep into that rabbit hole, let me give a little bit of grounding or anchor it to something else that already exists. The Elixir community has LiveView, which embodies the idea of what if we ran the entire front end as an actor on the BEAM. But this is for entire applications.

So you would have a live view application, and then what you do is send a minimal runtime to the client, which sets up a WebSocket connection. Your entire front end runs on your back end and just sends DOM patches, essentially HTML divs, that get live updated. The front end simply indicates when a user clicks a button, and the back end sends back the necessary updates.

You need to insert a new div here. Exactly. This is how LiveView works. LiveView encourages you to build your entire application this way, which is a very popular approach for Elixir developers because they specifically want to avoid dealing with the front end, right?

This lets them reuse everything they already know about Elixir to create interactive front-end experiences, which is great. I think it's really, really cool tech, and it's very, very exciting. It’s kind of confusing that it never really caught on outside of Elixir. I think there's a PHP library that does something similar, but I believe it has a glaring problem: now your entire front-end interactivity is mediated over a WebSocket connection, which seems absolutely wild to me as a front-end developer.

As people in the UK who have interacted with services that primarily exist in California, the latency is suboptimal the further away you are. Exactly. I think this approach is very popular in places where there is excellent internet, and it works well for internal tools. Yes, with good internet, you wouldn't notice the difference. Exactly.

So, Lustre has essentially looked at that and thought, "Well, that is an excellent idea." We already have this concept of components, which is compatible with how things work on the beam. Instead of shipping our entire front end to the back end and running it over a WebSocket, why don't we take the idea of a component and say, "You can render it on the client as a web component, or you can run it on the back end as a server component." This allows for a seamless experience where sometimes it's actually running in the browser, and sometimes it appears to be running in the browser, but it's actually DOM patches going over the wire. Exactly.

The idea is essentially that, in cases where you aren't writing platform-specific code—of course, if you use an Erlang library, you can't run that in the browser; that's fair, and I'm not going to solve that—if you write your application in a platform-agnostic way, you can provide alternative implementations for different platforms. For example, if you did use an Erlang library, you could also provide a JavaScript implementation. The idea is that you can take that application, which includes a Gleam type that encapsulates your application's update function and view, and the only thing that needs to change is how you start it.

You can start it as a single-page application, just by calling `Lustre.start` in the browser. You can register it as a web component or register it on the back end as a server component. The idea is that your application code never changes; you could put it in a separate project or publish it as a package and say, "Hey, you can choose where this runs." Would you also be able to start off by saying, "I'm trying to think of a scenario here." So, I would begin with something that's a local intranet project, and I would render it entirely on the server side.

Eventually, I decided to spin out part of it to the public, and then 18 months down the line, I started to push some, if not all, of the application out to be genuinely on the front end. Sure. This is when you would lean into components, so we had that discussion, right? We were kind of debating whether the idea of encapsulation of state is a good or a bad thing, whether this is a naughty or a dirty thing. But actually, when you draw that hard component boundary, you say this is a black box that is encapsulated. The benefit of making a black box is that, from the parent's perspective, it doesn't need to know internal details. So, if you wanted to start with your application fully rendered on the client, for example, as a single-page application, you can do that. Then you go, "Okay, actually, I have some complicated piece of functionality, so I'm going to make that into a component," because there are engineering benefits to doing that, right?

It's easier for me to think about this as an encapsulated thing. Mm-hmm. And then, nine months down the line, you think, actually, this would be way better if I could just run it next to my database. Because you already drew that component boundary, the parent no longer cares. It's not the parent's concern where the component is rendered anymore.

It just says, "I want to render my counter component." The counter is a really poor example because you would never actually create a counter component. But you get the idea. At that point, it's just an implementation detail of the counter regarding where it renders.

So you just say, "Okay, well, now I'm going to swap out that for a server component, and I'll move my counter to the backend." What does that actually involve in practice? I mean, what things am I typing to make it run on the backend instead? In Gleam projects, full stack Gleam projects are essentially organized as a monorepo. So, you have a backend Gleam app and a frontend Gleam app.

And typically, I suggest that people have a third Gleam project for their shared code, including types and serialization, and other elements that they want to share between both projects. Let's say you have this counter component. It's in your front-end project because that's where it was originally placed. So, you have a module that includes the init, the update, and the view functions.

It's all neatly bundled up into its own thing. Currently, in the client main setup function, you have a call to `Lustre.register`, where you specify it as "my component" and provide your component app. Now, that is registered as a custom element. The process of moving that to the backend involves removing that call because you no longer need to register it as a custom element.

Copying that counter is probably to your shared project now, or you could move it straight to the backend. You would move the code somewhere else so that you have access to it from your backend. Then, from your backend, you set up a WebSocket handler. When that WebSocket initializes, you call the Lustre Start Server component. Now, that's been an actor that communicates over a WebSocket.

And then the only thing that changes in the client is that you render a custom element called "Lustre Server Component" and point it to your WebSocket endpoint. If you organize this properly, that would essentially be hidden through a function call. You would have a function that is like `counter.view`, right?

Just from the counter module, you would call that, or counter.element, or whatever you call it. You would just swap out the implementation there, so the parent doesn't even need to change at that point. Okay, so let me ask you a couple of quick practical questions on that. Do I need one WebSocket per component, or am I reusing a single WebSocket across the whole project? Currently, well, actually, compared to LiveView, this whole approach in Lustre takes a step back from being tied to any particular web server or data transport protocol.

And so you could actually use the same WebSocket for anything because it's your responsibility to essentially hand the data from the WebSocket to the server component runtime. This is a negative in the sense that it involves a bit more boilerplate for the author, but it's a positive because now you are in complete control over how that works. You could swap out the WebSocket for HTTP long polling, server-sent events, or, I mean, if you were crazy, you could wire the whole thing up as a REST API. The server component runtime doesn't actually care.

Okay, I'm going to run it over a Kafka topic then. Or RabbitMQ in a queue, something like that—something crazy. Yeah. Typically, you would use one socket per component, but only because that would be the easiest or least complicated approach. However, there's nothing stopping you technically from using one socket for all components on the page.

Okay, okay, so multiplexing is my job, but it doesn't sound like that hard of a job. The other question is sessions. Are you automatically taking care of the fact that 50 front-end users are running 50 components on your server? No? Okay. At this point, basically, what Lustre gives you is an actor that represents your character.

Your application runtime, and then the rest of the job, is similar to Luster's job, which is purely about rendering. Essentially, I'm expected to be a back-end author. Sorry, exactly. Yes, Luster really focuses on the front-end application aspect. The fact that it's running on the back-end is more of an implementation detail. As an author, you might not know much about front-end rendering, so you use Luster. However, you know more than I do about building your back-end, so I won't have opinions on that.

I think this is a benefit. Gleam's general approach, both in terms of language and community, is to minimize magic. This often manifests as slightly more work for the user and a bit more boilerplate. However, if you can get over that initial gut reaction of, "Oh, boilerplate is the worst; I want all the fancy things to do it for me," you'll find that the end result is a very simple experience, one that provides you with much more control.

Generally, in the long run, I favor that. Actually, what you get is a very simple experience, one that you have a lot more control over. Yep. Okay, so what about? I should do a couple more checks on the practicalities of this.

So what about continuously nesting? It sounds like I'm okay if I take a client-side component that contains a server-side component, which in turn contains a client-side component. Is it just going to work? Exactly. It will just work.

Okay, okay. That's an easy one, which I think is, like, so magical. Is it just going to work? Exactly.

It will just work. Okay. Okay. I think that is so magical. I believe that is the really cool part.

The idea or scenario that I have in my head, which I always tell people, is like having an admin dashboard. It’s a single-page application because you want snappy navigation, and it makes sense to render this on the client side first. Inside that, you have, for instance, a really data-intensive table. Maybe it's a table for the million users registered to your startup or SaaS app. So, you create that table as a component to handle filtering, searching, and ordering, among other functionalities.

You just want to encapsulate that, so you make it a component. Inside that component, you render a table that includes a search input which handles focus trapping, autocomplete, and suggestions. You should make that a component itself, right?

So right now you have your single page up, a table component, and inside an autocomplete search. The really, really neat thing is that you can go, well, this table, you know, like I'm spending a lot of time making a whole backend service for querying my users, and I'm adding pagination and filtering. I have this whole REST API, and I'm doing JSON encoding and then decoding. So you go, okay, well, why don't I take this table and render it as a server component? Because then, you know, I skip out on that whole layer, and I can just make database queries and speak directly to the database.

Yeah, yeah, yeah. But the table is still rendering a client component, right? It's still rendering that autocomplete input. So, you have all of the benefits of client rendering there, such as not worrying about WebSocket latency at that point or anything like that, while getting all of the advantages of the renderer.

Being next to your database while still enjoying all the benefits of your application running on the client. Yes, you might have just found a solution to the age-old debate of thin client versus thick client. Each has its pros and cons, and you're suggesting that you can pick and choose which advantages and disadvantages you want at a component level. Yes.

And because components, um, because components are conceptually larger than they are in React, you don't get stuck in the same sort of debates or pitfalls that you might encounter if you had this ability in, say, Vue. It would be crazy if you could turn every single React component into a real-time entity that ran on the server, allowing for arbitrary nesting. That would just lead to an absolute explosion of complexity and engineering effort, right? Right.

Because components are working on a far too granular scale in those frameworks, Luster components encapsulate a lot more, right? Then, actually, some of the operational downsides to doing this—like there is a bit of setup—feel a lot lower because you're getting much more out of one sort of little component. Okay, so are you saying there's a natural push away from components? It's not like a law, as it almost is in Elm, but when you don't make it the easiest path, you don't get the same proliferation of components. However, the option is still there; you need the power.

Yeah, that's exactly it. And honestly, I find that when I'm creating things in Lustre, I'm using components sparingly. There is a bit of setup involved, and also, you just kind of don't need it in, say, 80% of scenarios.

The Elm approach works really well for about 90% to 95% of cases. So, don't fix what isn't broken. However, the issue is that the remaining 5% is where you really do want a component, but you can't have it. Yes, okay.

Do you have any guidance on where those boundaries lie? This is a question that every framework faces: what constitutes a component boundary? I'm not sure I have any real rules at the moment. The framework is still young, and the concept of components is also still evolving. When I first started with Lustre, we didn't have components.

It was a pretty faithful Elm implementation. The framework is now technically about a year old, but practically it's more like six to eight months old. I think there's a lot of learning as a community to figure out where those boundaries are. There is some overhead to setting up a component; you do need a message type. I believe the message type is the crucial element that indicates whether something really needs to be a component or not. If you only have, say, two messages, you're not really encapsulating a lot of complicated interaction here.

And so the benefits of setting this whole thing up as an application are quite slim. You've gone through all that work just to hide the fact that this component responds to button clicks or something. Yeah. But as soon as, like, I don’t really want to give a precise number because I don’t think that’s the kind of heuristic that is practical. However, as soon as you get to, say, five messages—five, seven, or ten messages—at this point, you’re clearly dealing with something that operates in its own world and receives messages from the outside world specific to it.

And then I think it starts feeling a bit more natural to turn this into a component. Yeah, I see that. Which is a very unsatisfying way of saying I'm not entirely sure what that boundary is. But I do know that Lustre pushes that boundary further away than React, Vue, or Svelte do. I think that's a good thing.

Yeah, it puts it on a high shelf that you can still reach, but you won't have it. It's like that trick where you put the biscuits or cookies on the shelf you can't quite reach. Exactly! If you really want the cookie, you can get it. However, you won't do it unthinkingly. So, I have one more question about this in practice: if I haven't really had a great experience trying to write web components and I think, "Okay, I'm going to get into this just for the one feature of hopefully an easy way to make web components and publish them in a language of my choice for other people who don't care what language it's written in to use," is Lustre on Gleam a good choice? It's certainly a choice.

I would say that this functionality, or this particular scenario, is something that I had in mind when I was working on this feature. To rewind a little bit, Lustre has a companion dev tools package that runs on Erlang. The full process here involves most of these developers not needing Node or npm, which is a selling feature. There's a dev tools package for Lustre that includes a bundler, a dev server, and some other features. Relevant to this conversation, it specifically includes a command to bundle a web component. This means you can take any Lustre component and say, "Hey, build this as a component," and then you get a JavaScript bundle that you can simply include on any web page, and it will just work.

And then I just write a regular old HTML tag, and it's magic. Exactly. Okay, so there are some caveats. There are some caveats to that and some problems inherent to web components, I would say.

So, Lustre puts in a bit of work to ensure that you, as an author, don't need to know that a component is a web component. Styling web components can be challenging in the web component ecosystem. What I mean by that is the term "web component" refers to more than one specification. There’s the Custom Elements specification, which involves creating custom HTML tags with their own logic, etc.

It also refers to the idea of the Shadow DOM, which is a way of hiding what the component renders to the parent, so that you can't reach in and manipulate it using something like a query selector, for example. Iframes 2.0. Exactly. The Shadow DOM is excellent. It's really, really good.

But when you register a Shadow DOM, what you have actually done is isolate the styles of the component from the parent. This is both a blessing and a curse. It is excellent because it means you can create a component with its own styles and then drop it into a different webpage, guaranteeing that it's not affected by that page's styles. It looks exactly how you authored it.

Yes, but the problem, of course, is that if you want to use Shadow DOM in your web application, you may not be concerned about conflicting styles. Typically, as an author, if you are building these components in JavaScript, you need to pay attention to their styling. There are ways to pierce the Shadow DOM; generally, you would use CSS variables, but there is also a Shadow Root CSS selector. The point I'm making here is that you have to be aware of this and find ways to work around it.

So, Lustre does some work to work around this for you so that you never need to understand or even know. The way it manages this is by adopting stylesheets from the parent document. When it cannot do that, it clones them into the component. For example, if you have an external link tag for a stylesheet hosted somewhere else, that gets cloned into the component, ensuring that the component has access to all of those styles as well.

Which works great if you're building a Lustre app and you want components. It works less well if you're building a component without an app and you want it to be styled in a self-contained way. So I would put it back to you and anyone else who's interested in this scenario: please try it and then tell me what you need to make it work nicely because I don't think anyone is doing this right now. The functionality is there, and the dev tools have what you need to make it work well. Like I mentioned, the functionality exists, and the dev tools have had the forethought to include the way to bundle them, but I don't think anyone is actually doing it right now.

Right. We need to conduct a test. Yeah. Okay. Then that brings us to what must be the last and inevitable question.

If someone wants to get started with Lustre, where do we begin? The documentation is a key resource; I put a lot of effort into it. While it is still a work in progress, I have invested significant time into the documentation. If you go to lustre.build, it will direct you to the package API on Hex.

And there is a detailed guide, a long-form written guide that will take you from zero to a complete app that handles side effects, HTTP requests, and more. There’s also a quick start guide available. Additionally, there is a series of examples that progress in complexity. If you prefer not to do a lot of reading and just want to look at code, there are plenty of examples to explore as well.

And there are cheat sheets if you're coming from React, LiveView, or Elm. Oh, wow. Okay, you have done your homework. Yeah, I really want people to use this thing, you know. Cool.

So, one last question then: what are you using it for? Are you using it in Angular, or in anything in production? Or is it just that you're the library author sending it out into the world to be used? Right now, I actually don’t have the time to use it myself.

Outside of work, I'm spending all of my time developing the library, but I do know a handful of people who have gotten this into production now. I know some people building their startups around it. Yeah, I'm not using it that much right now, which is why I need more people, more experienced reports, and more users. Right.

Well, then hopefully this little conversation will inspire a few people to go and give it a try, and then track you down with feedback. Yeah, I hope so. At this point, we should probably let you get back to writing the library and let them get started. Hayley, thank you very much for your time. Thank you for having me.

Absolute pleasure. Cheers. Thank you, Hayley. If you want to give Lustre a try, there are links in the show notes. And if you're Googling for it, it's L-U-S-T-R-E.

If you want to give Gleam a try, Lustre is a very nice gateway tool into Gleam's world and, thereby, into the world of Erlang. So, give that a try. And if you just want a nice place to hang out, the Gleam Discord has some great people, including Hayley and more. As this episode goes live, I will be at the GOTO Conference in Copenhagen.

So just on the off chance, if you happen to be there, come and say hi. And if not, I have every intention of coming back with some new guests, so you won't miss out. So stay tuned for that. Make sure you're subscribed; we'll be back soon. I've been your host, Chris Jenkins.

This has been Developer Voices with Hayley Thompson. Thanks for listening.