Some people prefer to write JavaScript with React. For others, it’s Vue or jQuery. For others still, it is their own set of tools or a completely blank document. Some setups are minimal, some allow you to get things done quickly, and some are crazy powerful, allowing you to build complex and maintainable applications. Every setup has advantages and disadvantages, but positives usually outweigh negatives when it comes to popular frameworks verified and vetted by an active community.
React and Vue are powerful JavaScript frameworks. Of course they are — that’s why both are trending so high in overall usage. But what is it that makes those, and other frameworks, so powerful? Is it the speed? Portability to other platforms like native desktop and mobile? Support of the huge community?
The success of a development team starts with an agreement. An agreement of how things are done. Without an agreement, code would get messy, and the software unsustainable quite quickly, even for relatively small projects. Therefore, a lot (if not most) of the power of a framework lies within this agreement. The ability to define conventions and common patterns that everyone adheres to. That idea is applicable to any framework, JavaScript or not. Those “rules” are essential to development and bring teams maintainability at any size, reusability of code, and the ability to share work with a community in a form anyone will understand. That’s why we can web search for some component/plugin that solves our problem rather than writing something on our own. We rely on open-source alternatives, no matter what framework we use.
Unfortunately, there are downsides to frameworks. As Adrian Holovaty (one of the Django creators) explains in his talk, frameworks tend to get bulky and bloated with features for one particular use case. It could be the inclusion of a really good idea. It could also be a desire to provide a one-size-fits-all solution for anyone and everyone’s needs. It could be to remain popular. Either way, there are downsides to frameworks in light of all the upsides they also have.
In server-rendered world, where we work with content that’s delivered to the browser from a server, this “framework mission” is filled with skills, ideas, and solutions that people share with co-workers, friends, and others. They tend to adopt consistent development rules and enforce them within their teams or companies, rather than relying on a predefined framework. For a long time, it’s been jQuery that has allowed people to share and reuse code on a global scale with its plugins — but that era is fading as new frameworks are entering the fold.
Let’s take a look at some core points that any framework — custom or not — ought to consider essential for encouraging a maintainable and reusable codebase.
Naming conventions
Some might say that naming is the hardest part of development. Whether it’s naming JavaScript variables, HTML classes, or any other number of things, a naming convention has a big impact on how we work together because it adds context and meaning to our code. A good example of a naming convention is BEM, a CSS methodology created by the folks at Yandex and adopted by many, many front-enders. That sort of standardization and consistency can help inform our objective, which is to have a standard convention for naming JavaScript selectors.
This situation might seem familiar: you revisit a project after a few months and you see some behavior happening on the page — maybe something you want to modify or fix. However, finding the source of that behavior can be a tricky thing. Is it the id
attribute I should search for? Is it one of the classes? Maybe the data attribute for this particular element?
I think you get the point. What helps is having some kind of convention to simplify the process and make everyone work with the same names. And it doesn’t really matter if we’re going with classes (e.g. js-[name]
) or data attributes (e.g. data-name="[name]"
). All that matters is that the names conform to the same style. Surely enough, any framework you will meet will enforce a naming convention of some sort as well, whether it’s React, Vue, Bootstrap, Foundation, etc.
Scope
A developer will likely struggle with JavaScript scope at some point in time, but we are specifically focusing on DOM scope here. No matter what you do with JavaScript, you’re using it to affect some DOM element. Limiting parts of the code to a DOM element encourages maintainability and makes code more modular. Components in both React and Vue operate in a similar fashion, although their “component” concept is different than the standard DOM. Still, the idea scopes the code to a DOM element that is rendered by those components.
While we’re on the topic of DOM manipulation, referencing elements within the root element of the component is super helpful because it help avoids the constant need to select elements. React and Vue do this out of the box in a very good way.
Lifecycle
In the past, the page lifecycle was very straightforward: the browser loaded the page and the browser left the page. Creating or removing event listeners, or making code run at the right time was much simpler, as you would simply create everything you need on load, and rarely bother with disabling your code, since the browser would do it for you by default.
Nowadays, the lifecycle tends to be much more dynamic, thanks to state management and the way we prefer to manipulate changes directly to the DOM or load the contents of pages with JavaScript. Unfortunately, that usually brings some issues where you may need to re-run parts of your code at certain times.
I can’t tell how many times in my life I’ve had to play around with code to get my event handlers to run correctly after re-writing part of the DOM. Sometimes it is possible to solve this with event delegation. Other times, your code handlers don’t run at all or run several times because they are suddenly attached twice.
Another use case would be the need to create an instance of libraries after changing the DOM, like “fake selects.”
In any case, the lifecycle is important and limiting your code to a DOM element certainly helps here as well, since you know that the code applied to that element needs to be re-run in case the element is re-rendered.
Frameworks do the same with functions as componentDidMount
or componentDidUpdate
, which give you an easy way of running code exactly when you need it.
Reusability
Copying code from somewhere else and reusing it is super common. Who hasn’t used a snippet from a past project or an answer from StackOverflow, right? People even invented services like npm meant for one thing: to easily share and reuse code. There is no need to reinvent the wheel and sharing code with others is something that is convenient, handy, and often more efficient than spinning something up from scratch.
Components, whether it’s React, Vue, or any other building block of a framework encourage reusability by having a wrapper around a piece of code that serves some specific purpose. A standardized wrapper with an enforced format. I would say this is more of a side effect of having some base we can build on, than an intentional effort for reusability, as any base will always need some standard format of a code it can run, the same way programming languages have a syntax we need to follow… but it is a super convenient and useful side effect, for sure. By combining this gained reusability with package managers like Yarn, and bundlers like webpack, we can suddenly make code written by many developers around the world work together in our app with almost no effort.
While not all code is open-source and shareable, a common structure will help folks in smaller teams as well, so anyone is able to understand most of the code written within the team without having to consult its author.
DOM manipulation with performance in mind
Reading and writing to the DOM is costly. Developers of front-end frameworks keep that in mind and try to optimize as much as possible with states, virtual DOM and other methods to make changes to the DOM when it’s needed and where it’s needed… and so should we. While we’re more concerned with server-rendered HTML, most of the code ends up reading or writing to the DOM. After all, that’s what JavaScript is for.
While most of the changes of the DOM are minimal, they can also occur quite often. A nice example of frequent reading/writing is executing a script on page scroll. Ideally, we would like to avoid adding classes, attributes or changing the contents of elements directly in the handler, even if we write the same classes, attributes or contents because our changes still get processed by the browser and are still expensive, even when nothing changes for the user.
Size
Last, but not least: size. Size is critical or at least should be for a framework. The code discussed in this article is meant to be used as a base across many projects. It should be as small as possible and avoid any unnecessary code that could be added manually for one-off use cases. Ideally, it should be modular so that we can pull pieces of it out à la carte, based on the specific needs for the project at hand.
While the size of self-written code is usually reasonable, many projects end up having at least some dependencies/libraries to fill in gaps, and those can make things quite bulky really fast. Let’s say we have a carousel on a top-level page of a site and some charts somewhere deeper. We can turn to existing libraries that would handle these things for us, like slick carousel (10 KB minified/gzipped, excluding jQuery) and highcharts (73 KB minified/gzipped). That’s over 80 KB of code and I’d bet that not every byte is needed for our project. As Addy Osmani explains, JavaScript is expensive and its size is not the same as other assets on a page. It’s worth keeping that in mind the next time you find yourself reaching for a library, though it shouldn’t discourage you from doing it if the positives outweigh the negatives.
A look at a minimal solution we call Gia
Let’s take a look at something I’ve been working on and using for a while with my team at Giant. We like JavaScript and we want to work directly with JavaScript rather than a framework. But at the same time, we need the maintainability and reusability that frameworks offer. We tried to take some concepts from popular frameworks and apply them to a server-rendered website where the options for a JavaScript setup are so wide, and yet so limited.
We’ll go through some basic features of our setup and focus on how it solves the points we’ve covered so far. Note that many of the solutions to our needs are directly inspired by big frameworks, so if you see some similarities, know that it’s not an accident. We call it Gia (get it, like short for Giant?!) and you can get it from npm and find the source code on GitHub.
Gia, like many frameworks, is built around components that give you a basic building block and some advantages we’ll touch on in a bit. But don’t confuse Gia components with the component concepts used by React and Vue, where all your HTML is a product of the component. This is different.
In Gia, a component is a wrapper for your own code that runs in the scope of a DOM element, and the instance of a component is stored within the element itself. As a result, an instance is automatically removed from the memory by the garbage collector in case the element is removed from the DOM. The source code of the component can be found here and is fairly simple.
Apart from a component, Gia includes several helpers to apply, destroy and work with the component instance or communicate between components (with standard eventbus included for convenience).
Let’s start with a basic setup. Gia is using an attribute to select elements, along with the name of the component to be executed (i.e. g-component="[component name]"
). This enforces a consistent naming convention. Running the loadComponents
function creates the instances defined with the g-component
attribute.
See the Pen Basic component by Georgy Marchuk (@gmrchk) on CodePen.
Gia components also allow us to easily select elements within the root element with the g-ref
attribute. All that needs to be done is to define the refs that are expected and whether we’re working with a single element or an array of them. Reference is then accessible in the this.ref
object within the component.
See the Pen Component with ref elements by Georgy Marchuk (@gmrchk) on CodePen.
As a bonus, default options can be defined in the component, which are automatically rewritten by any options included in g-options
attribute.
See the Pen Component with options by Georgy Marchuk (@gmrchk) on CodePen.
The component includes methods that are executed at different times, in order to solve the lifecycle issue. Here’s an example that shows how a component can be initialized or removed:
See the Pen load/remove components by Georgy Marchuk (@gmrchk) on CodePen.
Notice how loadComponents
function doesn’t apply the same component when it already exists.
It is not necessary to remove listeners attached to the root element of a component or elements within it before re-rendering them since the elements would be removed from the DOM anyway. However, there can be some listeners created on global objects (e.g. window
), like the ones used for scroll
handling. In this case, it is necessary to remove the listener manually before destroying the component instance in order to avoid memory leaks.
The concept of a component scoped to a DOM element is similar in its nature to React and Vue components, but with a key exception that the DOM structure is outside of the component. As a result, we have to make sure it fits the component. Defining the ref elements certainly helps, as Gia component will tell you when the required refs are not present. That makes the component reusable. The following is example implementation of a basic carousel that could be easily reused or shared:
See the Pen Basic carousel component by Georgy Marchuk (@gmrchk) on CodePen.
While we’re talking about reusability, it’s important to mention that components don’t have to be reused in their existing state. In other words, we can extend them to create new components as any other JavaScript class. That means we can prepare a general component and build upon it.
To give an example, a component that would give us the distance between the cursor and the center of an element seems like a thing that could be useful someday. Such a component can be found here. After having that ready, it’s ridiculously easy to build upon it and work with provided numbers, as the next example shows in the render function, although we could argue about the usefulness of this particular example.
See the Pen ZMXMJo by Georgy Marchuk (@gmrchk) on CodePen.
Let’s try to look into optimized DOM manipulations. Detecting if a change to the DOM is supposed to happen can be manually stored or checked without directly accessing the DOM, but that tends to be a lot of work which we might want to avoid.
This is where Gia really drew inspiration from React, with simple, stripped down component state management. The state of a component is set similarly to states in React, using a setState
function.
That said, there is no rendering involved in our component. The content is rendered by the server, so we need to make a use of the changes in state elsewhere. The state changes are evaluated and any actual changes are sent to the stateChange
method of a component. Ideally, any interaction between the component and the DOM would be handled in this function. In case some part of the state doesn’t change, it won’t be present in the stateChanges
object passed into a function and therefore won’t be processed — the DOM won’t be touched without it being truly necessary.
Check the following example with a component showing sections that are visible in the viewport:
See the Pen Sections in viewport component by Georgy Marchuk (@gmrchk) on CodePen.
Notice how writing to the DOM (visualized by the blink) is only done for items where the state actually changes.
Now we are getting to my favorite part! Gia is truly minimal. The entire bundle containing all the code, including all the helpers, takes up a measly 2.68 KB (minified/gzipped). Not to mention that you most likely won’t need all of Gia’s parts and would end up importing even less with a bundler.
As mentioned earlier, the size of the code can rapidly increase by including third-party dependencies. That’s why Gia also includes code splitting support where you can define a dependency for a component, which will only be loaded when the component is being initialized for the first time, without any additional setup required to the bundler. That way, the bulky libraries used somewhere deep within your site or application don’t have to slow things down.
In case you decide one day that you really want to take advantage of all the goodies big frameworks can offer somewhere in your code, there is nothing easier than just loading it like any other dependency for the component.
class SampleComponent extends Component {
async require() {
this.vue = await import('vue');
}
mount() {
new this.vue({
el: this.element,
});
}
}
Conclusion
I guess the main point of this article is that you don’t need a framework to write maintainable and reusable code. Following and enforcing a few concepts (that frameworks use as well) can take you far on its own. While Gia is minimal and does not have many of the robust features offered by big players, like React and Vue, it still helps us get that clean structure that is so important in the long term. It includes some more interesting stuff that didn’t make the cut here. If you like it so far, go check it out!
There are lots and lots of use cases, and different needs require different approaches. Various frameworks can do plenty of the work for you; in other cases, they may be constraining.
What is your minimal setup and how do you account for the points we covered here? Do you prefer a framework or non-framework environment? Do you use frameworks in combination with static site generators like Gatsby? Let me know!
Wow, looks promising. I’ll try it asap! Thanks for the works and information
Did you look at t3js and if yes — what can you say about it (apart from it being a little outdated regarding a module system)? I recently researched some options for exactly this scenario (server rendered websites), and t3js was the only thing I found. Very interested to look further into gia
I hear about t3js for the first time to be honest. Apart from being outdated, as you say, it looks pretty neat. From what I got, it has pretty similar goals, but compared to Gia, I would say it’s enforcing the writing style/structure much more. Gia is more of a small peace of JavaScript giving you a head start and pushing you towards modularity, but still just small peace of JS you can work with and modify in any way.
We’re also looking into adding some more functionality for bigger tasks that require more control and separation of code into multiple components working together, to give it more of those “app” abilities when needed.
Great post, Georgy!
I want to hear more about overall website structure with Gia – for example does Pages extends components, nested components, Gia an Swup demo.