Writing Cleaner JavaScript with Modules
Honeybadger Staff
Posted on January 13, 2023
This article was originally written by Adebayo Adams on the Honeybadger Developer Blog.
Modules are one of the most commonly used JavaScript features because most JavaScript frameworks and libraries leverage the module feature for organization and componentization. Some developers think that the import and export keywords are ReactJS features.
In this article, I will explain how to encapsulate code using modules to make your projects cleaner. Let's take a look at what encapsulation is in the next section.
Encapsulation
In programming, encapsulation refers to bundling related code in a single source. The code can include associated functions and variables inside files or related files inside a folder.
Encapsulation is used to restrict direct access to data and implementations of the bundled and related code from the code that uses them. Thus, the implementation of the functionalities is hidden, can't be manipulated by other parts of the code, and will only change when you want to change it. For example, in a blog application, when you bundle all the post properties and methods in a single unit, you only need to give the code that needs to interact with it: the name of the functions and the values they need to work.
Encapsulation is necessary because it makes your code cleaner, maintainable, and easier to understand, reuse, and test.
Next, I'll explain what modules are in JavaScript.
Modules
A module is an independent, self-contained, and detachable unit of a program. JavaScript allows you to structure large programs and codebases by creating modules of code that hold related functions and properties that can be exported in multiple other files that need these properties and functions.
Code organization is the major reason a framework is a go-to option for most developers when building medium to large applications. I'll show you how to structure your code with JavaScript modules. First, let's look at the syntax of JavaScript modules.
Exports
Using modules makes functionalities available for other modules; the export
keyword makes this possible. You can make a function accessible by other modules:
export function verifyUser(email, password) {
// do all necessary checks
return "Successfully verified user!"
}
Note: Files that imports or export code are known as a module.
The above function is ready to be used in other modules that need the verifyUser
function. This is called a named export.
Exporting Multiple Properties
Using the export
keyword, you can export anything from variables to classes. To export multiple properties from a module, you need to prefix the declaration with the export
keyword:
export const userName = "Lee";
export const userAge = 30;
export const user = {
name: "Lee",
age: 30
};
The code above exports all the declarations in the module, but you don't need to export everything in a module; you can have declarations that are only available for use inside the module:
const apiKey = "12345";
export function getApiKey() {
return apiKey;
}
Note: A declaration is a function, class, variable, or anything declared inside a module.
The code above exports only the getApiKey
function, which returns the apiKey
variable declared above the function.
A cleaner way to export multiple properties is using the curly brackets notation:
// user sign in
function userSignIn() {
console.log("User signed in");
}
// user sign out
function userSignOut() {
console.log("User signed out");
}
// delete task
function deleteTask(id) {
console.log(`Task ${id} deleted`);
}
//add task
function addTask(task) {
console.log(`Task ${task.id} added`);
}
//edit task
function editTask(id, changes) {
console.log(`Task ${id} edited`);
}
//complete task
function completeTask(id) {
console.log(`Task ${id} completed`);
}
You can export all the functions in the block of code above:
export {
userSignIn,
userSignOut,
deleteTask,
addTask,
editTask,
completeTask
};
The above block of code exports all the declarations inside the curly braces and is available for imports in other modules, so now the module will look like this:
// user sign in
function userSignIn() {
console.log("User signed in");
}
// user sign out
function userSignOut() {
console.log("User signed out");
}
// delete task
function deleteTask(id) {
console.log(`Task ${id} deleted`);
}
//add task
function addTask(task) {
console.log(`Task ${task.id} added`);
}
//edit task
function editTask(id, changes) {
console.log(`Task ${id} edited`);
}
//complete task
function completeTask(id) {
console.log(`Task ${id} completed`);
}
export {
userSignIn,
userSignOut,
deleteTask,
addTask,
editTask,
completeTask
};
Note: The
export {...}
block of code is typically placed at the bottom of the module for readability, but you can be put it anywhere inside the module.
Default Exports
Sometimes, you might have a function over 100 lines of code and want to place it alone in a single file. To make importing it into other modules easier, you can make it a default export:
// google sign in
export default function googleSignIn() {
// 100 lines of checking and getting details
console.log("User signed in with Google");
}
The above function is being exported as the default from the module. I will show you how to use the import
keyword in the next section.
Note: A module can have only one
default
export.
Import
In the previous section, you learned about using the export keyword to make properties of a module available for other modules. In this section, I'll teach you how to use the import keyword to get code from other modules.
Importing
To use code from other modules, you can import them using the import
keyword:
import { userSignIn, userSignOut } from "./filePath.js";
Note: The
"./filePath"
is a relative path to the directory route.
The code above imports the userSignIn and userSignOut functions from the declared module. You can import one or more declarations; the only requirement is to ensure the property is defined in the module from which you are importing.
Importing default
Exports
In the "Default Exports" section above, you learned how to export functions as default from modules. This section will explain how to import exported declarations as default.
You can import a default export using the following:
import googleSignIn from "./filePath.js";
The above code imports the googleSignIn
function, which was exported as default in the previous section. Because a module can have only one default export, you can omit the name of the function declaration, and the above code will still work; this means you can declare the function without a name:
// google sign in
export default function () {
// 100 lines of checking and getting details
console.log("User signed in with Google");
}
The above code will work because it is a default export.
The only difference between importing a default and named export is the curly braces:
// default export, no braces
import googleSignIn from "./filePath.js";
// named export, must use braces
import { userSignIn, userSignOut } from "./filePath.js";
Namespace Import
Sometimes, you have a module containing many different utility functions and want to use a single name to access them; this name is called a namespace
.
For example, you have defined all user related functions in a module:
function getUserName() {
return userName;
}
function getUserAge() {
return userAge;
}
function getUser() {
return user;
}
function getApiKey() {
return apiKey;
}
function userSignIn() {
console.log("User signed in");
}
function userSignOut() {
console.log("User signed out");
}
export {
getUserName,
getUserAge,
getUser,
getApiKey,
userSignIn,
userSignOut,
};
Then, the module that will use the function will import it:
import * as userFuncs from './filePath.js';
The above code uses a special character *
to import all the declarations in the module on top of the userFuncs
. You can access the getUserName
in the module:
import * as userFuncs from './filePath.js';
// use getUserName function
userFuncs.getUserName();
Renaming Declarations
To help developers avoid naming collisions, JavaScript modules use the as
keyword to rename declarations.
Renaming Exports
Sometimes, you might have a declaration named login, and you're using another library that has a function named login; you can export your login function as myLogin:
function login(email, password) {
// check if your email and password are valid
return "User logged in";
}
// export as myLogin
export { login as myLogin };
The code above declares a login function but exports it as myLogin. You can import the function as myLogin:
import { myLogin } from "./filePath.js";
The above function imports the myLogin
function. Next, I'll show you how to rename imports.
Renaming Imports
When working on a large project, you will import from multiple modules, making it easier to mix up declaration names. For example, you might be working with two different libraries, one for Twitter authentication and the other for Google authentication, and both have their own login
function. To avoid naming collisions, in this case, you can import them with different names:
// import twitter login
import login as twitterLogin from 'twitter-auth';
// import google login
import login as googleLogin from 'google-auth';
The above code imports the login function of two different libraries with specific names. This way, it's easier to avoid bugs and helps other developers understand your code.
Next, I'll show you how to re-export a declaration.
Re-exporting
Although it’s not commonly used, JavaScript modules allow you to re-export a module you previously imported:
// import the login function
import { login } from './filePath.js';
// re-export the login function
export { login };
The above code imports the login
function and then re-exports it.
Now that you know how to use import and export, I'll show you how to structure applications using modules.
Structuring Code with Modules
In the previous sections, you learned how to use the import and export keywords to make code available in different and multiple modules. In this section, I'll explain the benefits of using modules and how they help in structuring your code and applications.
Reusability
Whether you're a beginner, intermediate, or advanced developer, you've probably seen the term "DRY" or "Don't Repeat Yourself" on the internet.
What this means is that most times, you can reuse functions multiple times in different parts of the code. As you have learned in the previous sections, modules make this easier because all you need to do is write the code, export it, and then use it in other modules that need the particular function.
A few of the benefits of this approach are as follows:
- Saves time.
- Increases the maintainability and portability of the code.
- Increases the productivity of developers.
- Reduces redundancy.
These are just a few benefits of reusability that using modules helps you achieve.
Composability
Composability allows you to break functionality into pieces and bring them together to form the whole function, as well as allow you to reuse the parts of the function in other parts of the application.
An example of this is when creating an addComment
function, you might want to make some checks inside the function:
- Is this user allowed to comment?
- Remove prohibited characters like
<h1></h1>
from input. - Is this input length greater than the allowed characters?
- Add input to the database.
Then, you can create these four different functions, for example:
// check if user if allowed to comment
export default function canComment(user) {
// make checks here
return user.signedIn;
}
// check if input contains html tags
export function containsHTML(input) {
return /<[a-z][\s\S]*>/i.test(input);
}
// check if input is not longer than maxLength
export function isTooLong(input, maxLength) {
return input.length > maxLength;
}
// add input to the database
export function addToDatabase(input) {
console.log(`${input} added to database`);
}
The above functions can now be combined to create the addComment
function:
function addComment(user, comment) {
if (canComment(user) && !containsHTML(comment) && !isTooLong(comment)) {
addToDatabase(comment);
}
}
Each function that makes up the addComment
function can also be used independently in other parts of the program.
The benefits of composability include the following:
- It makes your code cleaner.
- It makes it easier to reuse existing code.
- It makes it easier to separate concerns.
- It makes code easy to understand.
Isolation
Understanding the whole project can be difficult for new team members working on a large project.
Because modules allow you to build the application by composing small, focused functions, each of these functions can be created, repaired, and thought of in isolation.
Using the example in the previous section, to change the implementation to check whether the user can comment, you only need to modify the canComment
function. The rest can remain unchanged.
Isolation makes it easier to understand, modify, and test your code.
Readability
Using modules in your code makes it easier to read. This is especially necessary when working on large applications, and it's almost impossible to explain to each developer on the team what you're trying to do with a function.
For example, without going into each file to see the implementation, a developer almost automatically knows what the following function does:
function addComment(user, comment) {
if (canComment(user) && !containsHTML(comment) && !isTooLong(comment)) {
addToDatabase(comment);
}
}
The code above can be read as, "If the user can comment, the comment does not contain HTML, and the comment is not too long, add the comment to the database." This makes it easier for new team members to start contributing to the project, which saves time.
Organization
When using modules, organization occurs almost automatically because each part of the code is isolated.
For example, you might have all the functions used to check the type of declarations inside a typeUtils.js
:
// check if input is a string
export function isString(input) {
return typeof input === "string";
}
// check if input is a number
export function isNumber(input) {
return typeof input === "number";
}
// check if input is an array
export function isArray(input) {
return Array.isArray(input);
}
// check if input is an object
export function isObject(input) {
return typeof input === "object";
}
// check if input is a function
export function isFunction(input) {
return typeof input === "function";
}
// check if input is a boolean
export function isBoolean(input) {
return typeof input === "boolean";
}
// check if input is null
export function isNull(input) {
return input === null;
}
Without giving it much thought, the above code is organized, as they are all related and independent of one another.
Conclusion
I hope you enjoyed this tutorial! Hopefully, you better understand how using modules in JavaScript can improve your code. In this article, you learned what encapsulation is, what modules are and how they function, as well as explored how export and import keywords work and how to rename declarations. Finally, you learned how modules can help structure your code.
Posted on January 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024