BYOF: Build Your Own Framework

Published 2025-02-24, About 9 minute read.

The platform is getting so good. You may remember when I gushed over how it's now easy to make a dropdown web component. We're hopefully getting signals natively. And someday we might get declarative HTML rendering.

But the native features avilable to us today nearly cover all we need to make a component framework ourself! To build your own component system/framework you need:

Let's try assembling something and see how close to the metal and simple we can be...

But hhwhy?

...you may ask.

And I get it. We have super nice things in web components already. We can use Lit and get everything we need. We could build in vue, or svelte, or preact and just compile to web components.

But this comes from an interesting place. While I was writing a post on stores, I found that I was getting into some framework concerns like declarative templating and reactivity. Instead of pulling in Lit, I was creating super simple components in those cases to keep things simple, and it I found it really wasn't hard to accomodate. The same happened when I was working on an article with services.

It turns out it's not hard to find the right utilities and platform abilities to get what you need done, done. I think it shows you why frameworks are the way they are- why things like signals and react hooks function the way they do.

Also, it's just a fun exercise. (At least, I think it is.)

So let's restrict ourselves to vanilla js modules in the browser, and try to stick to super simple primitives that do one thing and do it super well, and let's see what we can do.

Let's handle the component lifecycle part

This is the easiest. For this example we're just going to use web components and their lifecycles: constructor, connectedCallback, disconnectedCallback

Simple enough.

One thing to note: rendering should happen on the connectedCallback phase of your component's life. Why? Well, there are certain things that you can't do in the constructor according to spec. When the constructor runs you can in practice make the DOM for your component, and calling the element in the HTML will not error. BUT, if you ever need to construct an element and imperatively manipulate it and place it in the DOM, the browser enforces "no children or attributes" being made in the constructor when you try to create an element with document.createElement().

The exception to this is adding children inside a shadow root... but probably still not great practice. Beacuse, even if we forgot about the spec, your component won't have access to some functionality because it's not yet connected to the DOM. For example, you can't dispatch events on a document fragment that isn't attached to the DOM. Or, you can, but you won't get the results you'd like. Until it's connected do the document, global things like styles might not affect your component template. You wouldn't be able to get accurate style calculations with getBoundingClientRect(). You might have unexpected slot content issues.

It's a finer point, but TLDR let's just render in connectedCallback, agreed?

Now for a rendering strategy

The next part answers the question:

How should we render and update DOM, event handlers, atttributes, etc., when state changes?

If you trust yourself enough, and say you've lived through the jQuery years and actually didn't like switching to React back in the day, you might be able to make DOM parts, put them in the DOM as needed, handle template changes, handle setting up and tearing down handlers, and still have a smile on your face at the end of the day.

If that's you, I'm happy for you, and you don't need to read the rest of this article because native imperative web components are what we have already and it sounds like they are already probably your jam.

In my humble opinion, I would say that it's better for developer's mental health and overall wellbeing to use a library like lit-html, hyperhtml, lighterhtml, uhtml, or any other number of templating libraries out there. I tend to lean into the tagged template strategy instead of say JSX because I don't need any special build process that magically turns my templates into javascript. I like that the tagged template approach is completely runtime and just as nice to work with as JSX.

In the future, we should be able to enjoy something similar and native, like Template Instantiation or DOM parts.

We're going to use lighterhtml because it allows us declaratively interpolate values, attach DOM event listeners, and update the rendering surgically. We don't have to go in ourselves to find what part has changed and imperatively change those parts- the template renderer smartly diffs the DOM and does it for us.

So this is what that could look like...

We've pulled our template out and made a render() function. Then, in connectedCallback we use the lighterhtml render funtion to attach the template to this, the host component. Want to do that with a shadow DOM? Easy!

This takes care interpolating values, and adding event listeners. (In this case, to make a click event, you would bind the event listener to the onclick attributes. The details are particular to the rendering library- ours being lighterhtml).

What if we want to react to state changes?

It's just a little more work here- we need to create an "update" function that can be called to render the template every time there is a change. Here's one way to wire that up:

It still blows my mind that this library keeps a reference to the rendered DOM and only changes what needs to be changed.

This example demonstrates template rendering, adding event listeners, methods, and properties that work together inside the component, and how to ask for a rerender when the component state is changed. We don't have to be specific about what changed, we can just call this.update() and the renderer will take care of smart updates. This is the goal with delarative components.

The downside? We have to let the component know when state has changed. Wouldn't it be nice if we didn't have to do that?

The final piece: reactivity

A quick and dirty way to do this might include class setters and getters to trigger state updates when the setter is called

Now, instead of triggering renders in toggleState, the component triggers update when the actual state is changing. This makes it nicer for the user of this component to not have to worry about when to trigger updates, but adding all those lines for one state change is annoying for a developer to do for each reactive thing.

Another way which requires more understanding of javascript fundamentals and more work to set up, is to create a function that sets up those setters and getters for the user on the object. For example:

That works fine! You could have this property "setter" utility handy to make it easier to create reactive properties when the need arises. But something still doesn't feel great. It's not super clear that the resulting property exists on the component. You'll see this pretty quick if you're using TS, because you'll have to make a declare state: boolean property on the class to make Typescript trust you that this property exists. And the utility function we use is annoying to have to include on every component.

We could use a Proxy...

And it works... but we're starting to get real complicated here, and this proxy needs you to pass the update function to call on every state change. I'm not sure this implementation is making our life easier.

How about we use something already made, like preact signals? They are agnostic enough for us not to worry about depending on preact. They have the same niceties that our proxy example had, but signals are actually a much nicer experience. Plus, signals may be native eventually. So why not pull this kind of primitive in?

Here's how I would use it

Nice. We now have a way for users to make a reactive state. They have the flexibility to put it outside a component, inside a component, or in a module- anywhere, really. And the signals should handle the selective reactivity for us.

Tying it all together

As you probably know, I'm a huge fan of Lit. I think it is the most tried and true bulletproof framework for web components we have.

But I have a few issues still with it.

I've never been a huge fan of decorators because of the paradigm they introduce and the discrepancies between implementations between babel, typescript, and native browsers. So I wanted to avoid them. In Lit, you kind of sign up to use them- at least, the best experience is if you use typescript and use the decorators. Alternatively, you can do everything in lit using static configurations in vanilla javascript, but I wanted to avoid those too if I could and make everything as concise and simple as possible.

So I made a mini-framework for components called minne

For the most part it's based off of the strategy we walked through in this post: we use lighterhtml and preact signals in web components to make our lives easy. But it also goes well past that and handles some other things for web components:

Check it out and let me know what you think!

In conclusion

They say that creativity comes not from full freedom, but from working inside of restrictions. This is what I found when I tried to keep my web component demonstrations as vanilla and simple as possible. It was cool to see how much we can do by assembling the component, template, and reactivity ourselves.

Hopefully this has been fun and inspires some creativity and scrappiness in your coding. Let me know what you think. Find me on Bluesky or Mastodon. I also have an RSS feed here

⬅️ Previous Post
Back to top