Learn how to build scalable and reusable apps with ES6 modules

ES6 modules

One of the largest challenges when building a web-app is how quickly you can scale and respond to market needs. When the demand (requirements) increases, the capabilities (features) increase too. It is therefore important to have a solid architectural structure so that the App grows organically. We don’t want to end up in situations where the app can’t scale because everything in the app is deeply entangled.

Write code that is easy to delete, not easy to extend.


– Tef, Programming is Terrible

In this article, we’ll create a simple dashboard using ES6 modules, Optimization techniques for improving the folder structure and ease complexity. Let’s see why ES6 modules are important, and how to apply it.

JavaScript has had modules for a long time. However, they were implemented via libraries, not built into the language. ES6 is the first time that JavaScript has built-in modules (
source).

TL; DR — Want to see a practical example where we create a dashboard using ES6 modules, jump to Section 4.

Here ‘s what we’ ll address

  1. Why ES6 modules are needed
  2. Back in the days when scripts were loaded manually
  3. How ES6 modules work (import vs export )
  4. Let’s build a dashboard (using ES6 modules)
  5. Optimization techniques for ES6 modules

If you want to become a better web developer, start your own business, teach others, or improve your development skills, I’ll be posting weekly tips and tricks on the latest web languages.

1. Why ES6 modules are needed

Let’s view a couple of examples as to why we need ES6 modules.

Don ‘t reinvent the wheel

As developers, we often recreate things that were already created without being aware, or copy and paste stuff to reduce time. In the end, it adds up, and we are left with x number of identical copies scattered throughout the app. Each time we need to change something, we must do it x times depending on how many copies we have.

Example

For instance, imagine a car factory trying to reinvent the engine every time it produced a new car, or an architect starting from scratch after each drawing. It’s not impossible to do this, but then what is the point of knowledge and experience?

Knowledge barrier

If the system is deeply entangled, and not documented, it makes it difficult for new developers to learn how the app works.

Example

For instance, a developer should be able to see what the outcome of a change is without guessing, otherwise we end up with lots of errors without knowing where to start. By using modules for encapsulating behavior, we can easily narrow down the debug process and quickly identify the root of the problem.

I’ve recently written an article about
Developers That Constantly want to learn New things, with tips on how to improve knowledge.

Unexpected behavior

By avoiding the design principle of separation-of-concerns, it can lead to unexpected behavior.

Example

For instance, let’s say someone increases the volume in the car, and that action starts the windshield wipers. That is an example of an unexpected behavior, and not something we want in our application.

The end goal

In short, we need ES6 modules in order to effectively reuse, maintain, separate and encapsulate internal behavior from external behavior. It’s not about making the system complex, but making it possible in order to scale and delete stuff without breaking the system.

2. Back in the days when scripts were loaded manually

If you’ve done web development for a couple of years, then you’ve definitely encountered dependency conflicts such as scripts not loading in the right order, or that the elements of the DOM tree cannot be accessed through JS.

The reason is that the HTML on a page is loaded in the order in which it appears, which means we cannot load scripts before the content inside the <body> element is loaded at first.

For instance, if you try to access an element inside the <body> tag using document.getElementById("id-name") and the element is not loaded yet, then you get an undefined error.

The old fashioned way of solving such an issue was to load the scripts right before the </body> element like this:

Loading scripts in index.html

Another way to make sure that scripts are loaded properly is to use defer and async. The former will make sure that each script loads in the order it appears, while the latter loads the script whenever it becomes available.

However, in the long run, the number of scripts adds up and we may end up with 10+ scripts while trying to maintain version and dependency conflicts. Luckily we have great package manager tools such as NPM to structure JS packages by inserting them in the package.json file.

However, let’s see if we can manage the scripts in a simpler and more intuitive way using ES6 modules.

Separation-of-concerns

In general, loading scripts as shown above is not a good idea in terms of performance, Separation of dependencies and maintainability. We don’t want the index.html file to have the responsibility of loading All the scripts — we need some sort of structure and separation of the logic.

The solution is to reveal 6’s import and export statements, an elegant and maintainable approach that follows the design principle of separation-of-concerns.

Why import and export

The export keyword is used when we want to make something available somewhere, and the import is used to access what export has made available.

The rule of thumb is, in order to
import something, you need to first
export it.

So the question is, what can we actually export?

  • Variable
  • Object literal
  • Function
  • Class
  • ++

To simplify the example as shown above, we can wrap all scripts one file.

importing scripts

And then just load app.js script in our index.html. But first, in order to make it work, we need to use type="module" (source) so that we can use the import and export for working with modules.

Loading app.js in index.html

As you can see, the index.html is now responsible for one script, which makes it easier to maintain and scale. In short, the app.js script becomes our entry point that we can use to bootstrap our application.

Note: I would not recommend having all scripts loaded in one file such as app.js, except the ones that require it.

Now that we have seen how we can use the import and export statements, let’s see how it works when working with modules in practice.

3. How ES6 modules work

What is the difference between a module and a component? A module is a collection of small independent units (components) that we can reuse in our application.

What’s the purpose?

  • Encapsulate behavior
  • Easy to work with
  • Easy to maintain
  • Easy to scale

Yes, it makes development easier!

So what is a component really?

A component may be a variable, function, class and so forth. In other words, everything that can be exported by the export statement is a component (or you can call it a block, a unit etc).

What is a component

So what is a module really?

As mentioned, a module is a collection of components. If we have multiple components that communicate, or simply must be shown together in order to form an integrated whole, then you most likely need a module.

What is a module

Can we make everything reusable?

A principal engineer with over 30 years of experience in electrical engineering once said, we cannot expect everything to be reused because of time, cost and not everything is meant to be reused. It is better to reuse to some extent than expecting things to be reused 100%.

In general, It means that we don’t have to make everything Reusable in the app. Some things are just meant to be used once of thumb is that if you need something more than two times, then maybe it is a good idea to create a module or component.

At first, it may sound easy to make something reusable, but remember, it means taking the component out from its environment, and expect it to work in another environment. However, we often times have to modify parts of it to make it work fully reusable, and before you know it, you end up with more work than expected.

Antoine Stollsteiner wrote an article describing 3 essential rules of creating reusable JS components, which I recommend reading. When he presented VueJS to his team, an experienced coworker said:

That’s great in theory, but in my experience these fancy “reusable” things are never reused.

The idea is that, not everything should be reused. I’m referring to small stuff like buttons, input-fields and check boxes and so forth. The whole job of making it reusable requires resources and time, and often we end up with over-thinking scenarios that would never occur.

A 50%-good solution that people actually have solves more problems and survives longer than a 99% solution that nobody Shipping is a feature. It is because it’s in your lab where you’re polishing the damn thing Your product must have it. — CEO of Stack Overflow
Joel Spolsky

4. Let’s build a dashboard

Now that we have a basic understanding of how modules work, let’s view a practical example you’ll most likely encounter when working with JS frameworks.

We’ll be creating a simple dashboard that consists of layouts and components by using import and export statements.

For this example, I’m using the online IDE stackblitz.com, the code can be found here.

Step 1 — Design what you need

In most cases, developers would jump directly into the code. However, design is an important part of programming and it can save you a lot of time and headache. Remember, design is often not 100 % correct, but is rather an approach that leads you in the right direction.

Design what you need, and then create it. That’s a healthy approach, trust me!

Architectural design of our dashboard

Now that we have a clue of what we need from the design above, the next step is to structure the folders.

We need three folders:

  • Components –consists of users.js.user-profile.js and issues.js
  • Layouts –consists of header.js and sidebar.js
  • Dashboard folder –consists of dashboard.js which is a collection (a module) of components and layouts. We can also create different dashboard views if needed.

So why do we have a layouts and components folder? A layout is something that we need once, and typically is a static template. For instance the content inside the dashboard may change, but the sidebar and header will stay the same (and these are what is known as layouts). A layout can be either an error page, footer, status page and so forth.

The components folder is for the components we most likely will reuse more than once, or those that consist of dynamic data such as showing a list of users.

It is important to have a solid ground structure when dealing with modules. In order to effectively scale in the future, folders must have reasonable names that make it easy to debug and locate stuff. However, This is not the main purpose of this example — just an improvement tip.

Later I’ll show you how to create a dynamic interface, which requires having a folder structure.

Step 2 — Setup Folder structure

In most frameworks, you’ll most likely import and export a class because you want a state to be remembered, and that is what we’ll focus on here.

As mentioned, we have 3 main folders like this:

And in each folder we have a component that simply exports a class:

Step 3 — Implementation

The folder structure is all set, so the next thing to do is to create the component (a class) in each file and then export it. The code convention is the same for the rest of the files: every component is simply a class, And a function that Consoles “X Component is loaded” to indicate that the component has been loaded.

Let’s create a user class that makes the class available by using the export statement as shown below.

users.js (export at the bottom)

Notice, we have various options when we deal with the export statement. You can find them here. So the idea is that you can either export individual elements, or a collection, as long as you know what you’ll need to import in other files.

Source mozilla.org — Export JS modules

Alright, so if you look at the architectural diagram in step 1, you’ll notice that the user-profile component is encapsulated by the header layout. This means that when we load the header layout, it will also load the user-profile component.

Loads user-profile component when header layout is loaded

Now that each component and layout has an exported class, we then import it in our dashboard file like this:

In order to understand what is really going on in the dashboard file, we need to revisit the drawing in step 1. In short, since each component is a class, we must create a new instance and then assign it to an object. Then we use the object to execute the methods as shown in method loadDashboard().

Currently, The app doesn’t output anything because we haven’t executed the method loadDashboard(). In order to make it work we need to import the dashboard module in file index.js like this:

Loading Dashboard.js

And then the console outputs:

ES6 Components loaded

As shown, everything works and the components load successfully. We can also go ahead and create two instances and then do something like this:

Create two different instances of dashboard

Which outputs the same as shown above, but since we have to new instances, we get the results twice.

Two unique instances of dashboard

In general, this allows us to easily maintain and reuse the module in the files needed without interfering with other modules. We just create a new instance which encapsulates the components.

However, as previously mentioned, the purpose was to cover the dynamic of how we can work with modules and components using the import and export statements.

In most cases when working with JS frameworks, we usually have a route that can change the content of the dashboard. Right now, everything along such as layouts is loaded every time we invoke the method loadDashboard() which is not an ideal approach.

5. Optimization techniques for ES6 modules

Now that we have a basic understanding of how modules work, the approach is not really scalable or intuitive when we deal with large applications that consist of a lots of components.

We need something that is known as a dynamic interface. It allows us to create a collection of what we need, and then easily access it.

So if we have a module with twenty components, we don’t want to import each component one line after the other. We simply want to get what we need, and that’s it. If you’ve worked with namespaces in languages such as C#, PHP, C++ or Java, you’ll notice that this concept is similar in nature.

Here’s what we want to achieve:

As shown, we have fewer lines of code, We made it declarative without losing the context. Let’s see what changes we made to achieve it.

Create a dynamic interface (also known as a barrels)

A dynamic interface allows us to create a collection of things we need. It’s like creating a toolbox with our favorite tools. One thing that is important to mention is that a dynamic interface should not be added in every single folder, but to folders that consist of many components.

We just don’t want to have too many barrel files since They greatly simplify the imports and make them look good that is counter productive and usually leads to
circular dependency issues which sometimes can be quite tricky to resolve. 



Adrian Fâciu

In order to create a dynamic interface, we create a file named index.js which is located in the root of each folder to re-export a subset of files or components we need. The same concept works in TypeScript, you just change the type from .js to .ts like index.ts.

The index.js is The first file that loads when we access The root folder space — it’s The same concept as index.html That boots our HTML content. This means we don’t have to explicitly write import {component} from ‘./components/index.js’ , but instead import { component } from ‘./components.

Here’s how a dynamic interface looks. It’s not that difficult to create.

dynamic interface for components folder

By using a dynamic interface, we end up with one less root level to access, and also less code.

For more information on how to use index.js along with practical examples of how to structure your folder and naming, check this out.

Create a new instance at runtime

We removed the four instances in our dashboard.js, and instead created an instance at runtime when every component is exported. If you want to decide the name of the object, you can do export default new Dashboard(), and then import dashView without the curly braces.

As shown, we can directly invoke the method without needing to create a new instance, and also write less code. However, this is a personal preference and you can freely decide what is a practical use case for your app and requirements.

And finally, we load all components and layouts with one method.

Conclusion

I started with the intention of just showing a short example of how you can import and export a component, but then felt the need to share everything I know (almost). I hope this article provides you some insight into how to deal with ES6 modules effectively when building apps, and the things that are important in terms of separation-of-concerns (design principle).

The takeaways:

  • With ES6 modules we can easily reuse, maintain, separate and encapsulate components from being changed by external behavior
  • A module is a collection of components
  • A component is an individual block
  • Don’t try to make every everything reusable as it requires time and resources, and most often we don’t reuse it
  • Create an architectural diagram before diving into the code
  • In order to make components available in other files, we must first export and then import
  • By using index.js (same concept for TypeScript index.ts) we can create dynamic interfaces (barrels) to quickly access the things we need with less code and fewer hierarchical paths
  • You can export a new instance at runtime by using export let objectName = new ClassName()

The good news is that things have changed and we are moving towards a component-based and reusable paradigm. The question is how can we reuse not only plain JS code, but HTML elements too in a practical and intuitive way. It looks like that ES6 modules combined with web components may just give us what we need to build performant and scalable apps.