Latest updates in my newsletter.
JavaScript modules are a way to organize code into reusable components that can be shared across different files and projects. Modules can be imported and exported using two different methods: default exports and named exports. In this article, we’ll take a closer look at default exports and why you should avoid using them in your JavaScript modules.
What are Default Exports in JavaScript Modules?
Default exports allow you to export a single value from a module that can be imported with any name.
This value is the default export for the module and can be any type, such as a function, object, or primitive value. Here’s an example of a module that exports a default function:
// math.js
export default function subtract(a, b) {
return a - b;
}
In this example, the subtract function is the default export for the math module. It can be imported with any name in another file, like this:
// app.js
import add from './math.js';
const result = add(2, 2); // returns 0
Notice that we imported the subtract function as multiply. This is possible because default exports can be imported with any name. This can lead to confusion and make code harder to maintain as a codebase grows.
The Problem with Default Exports
Default Exports are Not Discoverable
One of the main problems with default exports is that they are not discoverable.
When you import a module that has only named exports, your IDE can show you a list of available exports that you can use in your code. However, this is not possible with default exports because they are not named. This means that developers may not know that a module has a default export, or they may not know what it is called. This can lead to confusion and make it harder to use the module.
VSCode shows the available list
Default Exports are Refactoring Unfriendly
Another problem with default exports is that they are refactoring unfriendly.
When you rename a named export in a module, any usage of that export can be automatically renamed as well, provided that you are using an IDE that supports this feature. However, this is not possible with default exports because they can be imported with any name. This means that if you rename a default export, you must manually update any usage of it in your code.
Named exports can be automatically renamed
Default Exports are Naming Nightmares
Default exports can also lead to naming nightmares. Because they can be imported with any name, different developers may use different names for the same default export. This can lead to inconsistency and make code harder to maintain as a codebase grows. In contrast, named exports provide a clear and consistent naming convention that makes code easier to understand and maintain.
Alternatives to Default Exports
Named Exports
Named exports are a good alternative to default exports. With named exports, you can export multiple values from a module and give each one a clear and consistent name. This makes it easier for other developers to understand and use your code.
Here’s an example of a module that exports two named functions:
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
In this example, we exported two named functions, add and subtract. These functions can be imported in another file like this:
// app.js
import { add, subtract } from './math';
const sum = add(2, 2); // returns 4
const difference = subtract(2, 2); // returns 0
Notice that we imported the functions using their names, which makes the code easier to understand and maintain.
Default Export Wrappers
Another alternative to default exports is to use a default export wrapper. This involves creating a separate module that exports a default function that simply imports and re-exports the desired value from the original module.
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// math-wrapper.js
// Choose what you want to export
export { add, subtract } from './math';
// OR export as a whole
export * as default from './math';
This wrapper is another layer of abstraction where you can select the modules you want to export from the associated folder or export all the contents of a file as default
. This way, you can use them in app.js
like this:
This also maintains clear and consistent naming, avoiding the pitfalls of default exports.
Necessity of Default Exports
There are no absolutes in everything, and default exports are necessary in some cases. For example, in common component encapsulation:
$ ls ./components
Row.jsx Form Upload.jsx
Select Alert.jsx Skeleton
...
Here, a folder or file represents a component, and in this case, you may have to use default exported components.
The recommendation here is to name the default exported component consistent with the file name and the component name within the file. This minimizes misunderstandings and errors and allows you to change names all at once in the IDE.
// app.js
import Row from './Row.jsx';
import Select from "./Select"
In addition, it is more recommended to use index.js
in the components folder to aggregate these components and use named exports:
// components/index.js
export { default as Row } from './Row.jsx';
export { default as Select } from './Select';
// app.js
import { Row, Select } from "./components";
Other Best Practices
In addition to the alternatives we discussed, there are some other best practices you can follow to improve the maintainability of your code:
- Use consistent naming conventions for your exports. This will make it easier for other developers to understand and use your code.
- Avoid exporting too many values from a single module. This can make your code harder to understand and maintain.
Conclusion
While default exports can be useful in certain situations, they should be used with caution and only when necessary. Again, this is just general advice. There is no one-size-fits-all solution for every project. Ultimately, it’s up to you to choose the most suitable method.