Default Exports in JavaScript Modules Are Terrible

@lloydjatkinson
25,946
Average developer experience of using a module from NPM
Average developer experience of using a module from NPM

This article has proved to be quite popular and Korea Frontend newsletter contacted me to ask if they could translate it into Korean. Translated version available here.

JavaScript modules have two ways to define an export: default exports and named exports. In this article, I want to explain why default exports are a poor practice and lead to a worse developer experience. Consider an example module containing both a named export and a default export:

export const add = (a, b) => a + b;

export default (a, b) => a - b;

Before considering how this would be imported and used, I would like to point out another problem that can affect the developer experience too. The exported member is the default for the module. It is essential to consider the impact of this. Why is subtract a default, but add is a named export? Perhaps this artificial example does not highlight this problem well.

However, consider a developer using a more complex module they are unfamiliar with. They may not know which export is the default. They may not even know that there is a default export. This leads to the developer needing to spend more time reading the documentation (assuming there even is any, this is the NPM ecosystem, after all 1) or reading the library source. If there were only named exports, then discovery becomes simple.

Using the IDE, we can see the exported members of a module immediately. So what is missing from this list? That’s right, the default export. Remember, default exports are not named exports; therefore, the IDE cannot display it in this list in a meaningful way, so it does not try to.

VS Code Intellisense

In fact, this reminds me of the incredibly poor developer experience of Node’s failed attempt at a module format - CommonJS. An easy way to tell if code is using CommonJS is if you see require and module.exports. Much like a default export, there will be little to no IDE assistance.

Now that I’ve finished that detour into IDE tooling (and uncovered the problem of missing names in terms of autocomplete) I will now expand on the usage of default exports once you have figured out how to import them.

  • The exported member can be imported with any name. That’s right, the subtraction function can be imported with any name, including multiply. This is going to lead to confusion, especially as the code base grows.

    import multiply from './math.js';
    const result = multiply(2, 2);  // results is now 0
  • As default exports can be exported with any name, multiple developers will probably think up different names and naming schemes for default imports. There will be no consistency! I encountered exactly this problem recently, I use some rehype modules for processing the markdown files I use for posts. Both lines of code were taken from their respective documentation but note that one has rehype at the start. Now it’s up to me to decide which convention I should go with.

    import rehypeExternalLinks from 'rehype-external-links';
    import addClasses from 'rehype-add-classes';
  • Default exports are also refactoring hostile. If I rename the named exported members in a module, then any usage will be renamed, too (provided you are using an IDE that can apply this refactoring). With default exports, this isn’t possible.

You may be thinking, “What do I do if two named exports from different modules have the same name?“. Fortunately, this is an easy problem to solve with import aliasing.

import { Article } from './types';
import { Article as ArticleComponent } from 'my-design-system';

Of course, while you still need to think of a name for the alias, it’s a much nicer situation to be in than totally inventing a name for a default export. This is still refactoring-friendly. ArticleComponent is a redundant name, especially when clearly working with a component imported from a design system component library. But as the classic saying goes, “naming things is hard”. But, I am happier to deal with a one-off name scoped to a single file instead of no name and then thinking of a name every time I import it.

You may also be thinking, “I use a framework or tool that pretty much requires me to default export a function or component”. You can work around this too by using the “index.js pattern”. This is where you create an index.js or index.ts file in the root of the directory and then export the default export from there. A good article covering this pattern can be found here.

If you’re writing a module, whether strictly for your code base/project or for NPM, please stop writing default exports.


Footnotes

Footnotes

  1. Of course, poor documentation is unfortunately common in the industry. JavaScript documentation in particular though seems to be generally poorer on average - this could be attributed to the lower barrier of entry to JavaScript, causing inexperienced developers to publish countless useless packages to NPM. I don’t believe the fact that JavaScript is a dynamic loosely typed language could be a factor in this simply because Python does not appear to have this documentation problem.

Share:

Need help with your software project? Let's talk

Stay up to date

Subscribe to my newsletter to stay up to date on my articles and projects

Support me on Kofi