How I prefer to set up frontend projects and why
mksunny1
Posted on April 23, 2024
Although I started my programming journey with fairly high-level languages like Java and Python, I grew to appreciate and actively seek out the benefits of working as closely to the platforms as possible. This usually leads to gains in power and performance. I can hear you screaming productivity. Depending on who you ask, this may or may not be valid in the cases of Java and Python. However the subject here is web frontends and I will be upfront and say that I lean towards the side of core technologies and applying best practices for achieving a high level of productivity without compromising on crucial aspects of frontend development like accessibility, familiarity, semantics, simplicity, maintainability and even performance.
As much as I dislike writing this, I have always struggled to like large Javascript and CSS frameworks. I learned React and Vue and had a look at many others but I never felt like building anything with them, because, like all the others, they take over my frontend. Sorry, I cannot do without the flexibility to change whatever I want in my web pages and I cannot learn a new language to do what platform JavaScript already provides.
Many frameworks also enforce a pattern of building HTML, and even CSS, from Javascript, which is bad for accessibility and even for collaborative frontend development (everyone now must know JavaScript to contribute). It is also bad news for those who create backends with programming languages other than JavaScript as they must rewrite all templates to utilise new interactive and similar features. Unfortunately, the Custom Elements spec is also a bit culpable in this instance.
Fortunately, there are enough platform facilities to make this a non-issue. For me instead of writing:
function component() {
return newCompoent()
}
I will write:
function component(componentToSetUp) {
// set up code
}
The first function means I must use Javascript to create the component, while the second one allows me more flexibility. I can choose to build it with JS also or I can load it as part of the page markup and use selection tools like querySelector
or getElementById
to obtain it. The rest of the functions will usually be for setup like adding event listeners, attaching children, applying local styles, responding to lifecycle events or interpolating variables. There are powerful JavaScript facilities for these and there is certainly no need to build a new language for them.
There is no need to talk about attaching event listeners as we have been doing this forever. It is also pointless to discuss attaching children to components and there will often be no need for this when the components are created in HTML. This leaves only the subjects of applying local styles, responding to lifecycle events and interpolating variables up for discussion. Stay with me.
JavaScript introduced the Shadow DOM API to enable encapsulation of both HTML elements and CSS stylesheets within specific components. So naturally this can be used for localising styles without the need for a framework. Apart from this, one can also use the adoptedStyleSheets property of elements to add encapsulated styles without even the need for Shadow DOM. So this is also a non-issue and it was almost pointless to include here.
A major selling point for mast frontend frameworks is that they help you write almost regular markup which contains javascript variables that magically update the DOM when they change. This is a fantastic idea, but unfortunately, it is not the final abstraction and has created unwarranted complexity, loss of flexibility, loss of semantics and many other issues on the front end. One of the most important things I picked up from reading great Lisp hackers like Paul Graham and Peter Seibel was to never relent in the search for the best abstraction.
I have searched for years for the right abstraction for reactivity in JavaScript. Until recently when I realised that nothing was missing. It has been there all along. It is as simple as calling one function to call as many others as we like. From here we can make a function that calls all the functions in an array. At the points where we want reactivity, simply replace the declaratively written variables with an array push(...) call. Continue to work on this until you can express everything concisely. Congrats, you have reactivity! This is reactivity but it is not something to be used incessantly because of the unnecessary hit to performance it can create. This is where knowledge and application of best practices become vital and will only really appreciate this when we understand what our code does under the hood. Frameworks make it harder to get here.
For lifecycle methods, the same points apply. In many cases, we will still be the ones to trigger these methods. However, in some cases, such as when developing reusable components, we can use the simple model of reactivity described earlier to achieve the same end. Still remember there is a good reason mutation observers exist instead of lifecycle events on standard DOM elements. Performance is important.
I also have a suitable answer for the subjects of code structure and native apps, but I believe this is enough for a first post. I have written a bunch of libraries I bundled together in https://github.com/mksunny1/eventiveness to make my points concrete. Cheers.
Posted on April 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.