[Intro to section]

In this section, we’re going to be learning the essential concepts to build our final project. But more importantly, we’re going to taking a deep dive into parts of the language that are necessary if you’re looking to build serious JavaScript applications and if you want to move onto building apps with frameworks. We’re going to see in our final application that using a framework to build JavaScript apps is ultimately optional, that with these tools and concepts, that we’ll have everything that we need to build projects on our own.

Our last project is going to have more features and be significant bigger as a result. What can we do instead of throwing all of our code into a single file?

With ES2015, JavaScript got a new way of splitting up its code into multiple files using a feature called modules.

[What do modules do?]

Modules allow us to spread out our app functionality, going from a single file to multiple files while still being able to share code between these files.

[Modules — similar to const]

You can think of modules as being the equivalent of const for our variables, but for our JS files. We use const for our variables not because it is most flexible, but it has the smallest degree of control. We use clearly defined variables that cannot be overwritten and do not change in their data type.

The same applies to modules. We isolate our JS functionality into separate files and modules help us keep each bit of our app clearly defined from each other. Yet we know that creating a JS app isn’t possible with out having variables interact with one another, modules are the way that larger parts of our JS app are going to interact with one another, yet live in their own clearly defined space.

[What is a module?]

A module is just a file. One script is one module. We can create our first module right now with an existing script declaration. All we have to do to is to set the type of the script to module:

<script src="app.js" type="module"></script>

Note that module can load each other and share code, but we're going to see that we can build entire apps with just a single module.

So using modules, we're still going to reference single JavaScript file that will be the main file in our app so to speak. But as our app file grows bigger, we can create additional files and share code around our app however we need, from file to file.

How do we share code now?

Well first, for our app, let's use a very simple example where we just want it to display the date to our users.

So we'll create an app class that returns some basic HTML. And this app class will just be responsible for creating the app:

class App {
  constructor() {
    this.render();
  }

  render() {
    document.getElementById("root").innerHTML = `
      <div>Date: </div>
    `;
  }
}

new App();

Now we want to display the current date. So let's create a function to do that. But as I mentioned, we are imposing clear responsibilities for different parts of our App. We don't want our app class to be responsible for everything, just for making the app. Instead, why don't we have an external function take care of creating the date?

This approach to have separate responsibilities is essential for having organized code and it is called separation of concerns. We don't want one class or data structure to be responsible for everything. Just a clearly defined task or set of tasks. If we know what role each bit of our code plays, it's going to be very easy to manage different parts of our app and get them to interact nicely.

So let's create a separate file called utils, which means utilities for short. This is a conventional folder within JS projects that holds functions that perform various tasks around our app.

[Importance of folder / file names]

As we build our next project, we're going to touch on a lot more common naming conventions for folders and files in JavaScript apps. Having organized, clearly specified folders and files are just as important to building projects efficiently as say, well name variables. Maybe even more important.

Then within the utils folder, we'll create a .js file called date.js that can hold not just the function we want to create, but any date related functions that we might want to create in the future.

Let's create a function called getDate and we can very easily get the current date by using the date constructor:

function getDate() {
  const date = newDate();
  return date;
}

Now the question is, how do we use this created function in our app.js file?

What if back in app.js, we just try calling getDate? What do you think will happen?

console.log(getDate()); // Uncaught ReferenceError: getDate is not defined

We get a referenceerror. For app.js, getDate doesn't exist. So we now know that using modules, created values aren't put on the global object.

In fact, modules are automatically put in strict mode. And we know from strict mode that this behavior of being able to put things of the window is regarded as an error.

We can easily confirm that we're in strict mode by console logging this. If it's undefined, meaning global assignment is discouraged, we are in strict mode. And that's the case here:

console.log(this); // undefined

[Modules have module-level scope]

In addition to being in strict mode by default, each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts. That's another reason why app.js can't see the getDate function.

So we can't and shouldn't use the global object to share code around our app. What do we do?

[import export]

With modules, there are two keywords that give us what we want: import and export.

  • import allows to import functionality from other modules
  • export keyword labels variables and functions that should be accessible from outside the current module

[How import and export are used with code]

It works like this, we export a variable or function from a module and from any other module in our app, we can import it. And there's no limit to how many files can import something exported.

There are two ways to export a variable or function. With just the keyword export or the keywords export default.

Each module can have zero or more named exports.

While on the other hand, each module can have at most one default export.

Let's try exporting our function first with the export keyword, just add it before the data itself:

export function

Easy enough. Then since we want to use it in app, we import it there with the import keyword:

import

And when we're just using the export keyword for a piece of data, this is called a named export. And we can have many named exports per file, we have to use a named import, where we must import that corresponding piece of data according to its name between curly braces:

import { getDate }

These curly braces give us a convenient way of organizing multiple imported values from a single file.

And at the end of the import statement we have to specific where it's from with the from keyword. And then using a string we specify the path to that file:

import { getDate } from "";

Most of the paths that go from one file to another are called relative paths. Looking at our directory, and starting our path from where the app.js file is to get the file where the imported code is, we see that we need to go into the utils folder and then reference date.js.

To go into a nested directory we start with './', then utils, and date.js:

import { getDate } from "./utils/date.js";

Note that we need to chain on the .js at the end when using modules with a script tag with type='module'. Modules that are referenced without the extension at the end are called bare modules. Some environments like Node.js allow it, but if we try to do so here, our import statement won't work. So make sure to always include .js at the end of your imports if you're using modules via a script tag.

And once we have that, we can use our imported code as we like:

render() {
    document.getElementById('root').innerHTML = `
      <div>Date: ${getDate()}</div>
    `
  }

Named exports and imports like we just used are great when we have files that export multiple things. Let's try also adding a named export for a variable instead of a function from date.js.

We'll make a variable to tell the year, and once again we can add export before it:

export const year = new Date().getFullYear();

And then import it into app. Try to do this on your own if you can:

import { getDate, year } from "./utils/date.js";

...
render() {
    document.getElementById("root").innerHTML = `
      <div>Date: ${getDate()}, Year: ${year}</div>
    `;
  }

Now since we have a couple of named exports now, note that we can use an alternate syntax to export everything from this file in a single place. If we have a very long file for example, it may be tedious to look for all of the exports throughout to see what we're making available to the rest of our app.

Since we can import all of the named exports within curly braces, we can export all of them within curly braces too:

function getDate() {
  const date = new Date();
  return date;
}

const year = new Date().getFullYear();

export { getDate, year };

And when we save, note that everything works as before. For imports, however, we have the power to change the names of our named imports as we like using the as keyword.

Take one or more named imports and add on as, where you can give them a different name afterwards if you like:

import { getDate, year as currentYear } from "./utils/date.js";

This technique is called aliasing. Try to use it when it makes you code more readable. Ultimately it is most helpful to solve variable naming conflicts. Where for example, we were importing a variable called getDate, but had a local function called getDate as well and wanted to use both:

import {
  getDate as getPresentDate,
  year as currentYear,
} from "./utils/date.js";

const getDate = () => {};

We can use aliasing with named exports as well, but its more common with imports because we usually have to resolve naming conflicts for a given file, not across our entire app.

[Default exports and imports]

Now let's cover the other way of exporting stuff from modules, default exports. modules can have at most one default export. default exports are for when you just have one thing in a file that you want to make available.

So let's revise our date.js to where we really only want to export the getDate function and instead use the year variable locally within that function.

Like export, we can just add export default before the function:

export default function getDate() {
  const year = new Date().getFullYear();
  const date = `${new Date()}, 'year': ${year}`;
  return date;
}

Now what's different about importing default exports is that we don't have to use curly braces, since there is just one thing to import, plus we can name it whatever we want when we import it without having to us as.

So we could use its created name getDate:

import getDate from "./utils/date.js";

Or we could name it anything else, as long as it doesn't violate JS naming rules for variables:

import getDateNow from "./utils/date.js";

render() {
    document.getElementById("root").innerHTML = `
      <div>Date: ${getDateNow()}</div>
    `;
  }

And viola. Our code still works!

[Don't have named exports and default exports together]

Note that you can import both named and default exports from the same file, but I would avoid mixing them. Generally you should stick to just one or another export style per module.

And also, note that when a module is imported into a file, it is executed only once.

So for example, we were to have multiple import statements, those after the first one would be ignored:

import getDateNow from "./utils/date.js";
import getDateNow from "./utils/date.js"; // ignored!

And that's really all you need to know to be effective with modules. We're going to get very comfortable writing them in our last app. It's what our project is going to hinge on for being able to share code.