DEV Community

Cover image for Avoid using default exports
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Avoid using default exports

As you may already know, there are two different ways to share functions in JavaScript: default exports and named exports. Default exports are used to export a single value from a module, while named exports allow you to export multiple values from a module.

Default exports are great when you want to export something specific from a module, like a function or a class. Named exports, on the other hand, are perfect for when you want to export multiple things from a module at once.

In this post, we'll explore why using default exports isn't recommended and should be avoided. To get started, let's take a look at some examples of both.

Named exports

An example of a named export is as follows:

// add.ts
export const add = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

In this example, we've exported a function called add from the add.ts module. You can easily import this function into another module using its name:

// app.ts
import { add } from './add';

console.log(add(2, 3));     // 5
Enter fullscreen mode Exit fullscreen mode

Default exports

Let's rewrite the example above with using a default export :

// add.ts
const add = (a, b) => a + b;
export default add;
Enter fullscreen mode Exit fullscreen mode

In this example, we have exported the add function as the default export. This function can then be imported in another module using any name of our choice.

// app.ts
import add from './add';

console.log(add(2, 3));     // 5
Enter fullscreen mode Exit fullscreen mode

Now that you have a good understanding of named and default exports, let's dive into the next section to explore the disadvantages of using default exports.

Poor discovery

Default exports can be confusing in complex modules. Developers may not know which export is the default or even be aware of its existence. This can cause delays as engineers must spend more time looking at documentation or even the source code to find the functions they need.

On the other hand, using named exports makes discovery much simpler. With named exports, developers can easily see the exported members of a module and their corresponding names. This is especially helpful when using an IDE, as popular IDEs allow you to use a shortcut (such as cmd + space) to suggest available functions from a given file. Unfortunately, this shortcut doesn't work when using default exports.

Suggest a named import

Refactoring

If you decide to rename a named export in a module, for example, changing the name of the add function to sum, most IDEs can automatically update all usages. This makes the refactoring process much easier.

However, with default exports, this isn't possible.

Auto-complete

As we discussed earlier, when a module provides named exports, we can easily select a specific function from the module by using a shortcut provided by the IDE. This feature not only saves time, but also helps the IDE suggest and auto-import the necessary functions as we type.

For example, if you start typing add, the IDE will display a list of available packages that provide the add function. All you have to do is choose the right package from the list, and the editor will automatically insert the import for you. It's that simple!

Auto-complete a named import

Inconsistent codebases

Default exports can be imported using any name you choose. This can lead to inconsistencies when multiple engineers are working on the same codebase and using different import names.

import add from './add';

// Other engineer could use another name
import sum from './add';
Enter fullscreen mode Exit fullscreen mode

Re-exporting

When developing an npm package, it's common practice to export the package functions in its entry point, which is typically named index.js or index.ts if the package is implemented in TypeScript.

To keep things organized, we often create separate files for different functions and then re-export them in the entry point file. For example, we might have files named add.ts and multiply.ts, each containing a function to add or multiply two numbers.

If we use default exports for those files, we need to specify the names of the functions that will be available in the final package.

// index.ts
export { default as add } from './add';
export { default as multiply } from './multiply';
Enter fullscreen mode Exit fullscreen mode

Using named exports is more convenient:

// index.ts
export { add } from './add';
export { multiply } from './multiply';
Enter fullscreen mode Exit fullscreen mode

What if you want to export everything? That's where wildcard exports come in – they make it even easier.

// index.ts
export * from './add';
export * from './multiply';
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using default exports can make it harder to find things in your code and lead to inconsistencies. Instead, named exports make it easier to discover which members are being exported and what their corresponding names are, especially when using an IDE.

It's recommended to avoid using default exports and instead opt for named exports whenever possible. By doing so, you'll have a more organized and maintainable codebase that's easier to work with in the long run.


If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

Top comments (27)

Collapse
 
brense profile image
Rense Bakker

Actually IDEs should not have any problem finding your default exports aslong as they are not anonymous functions or objects. If you use recommended eslint rules, it will tell you not to export anonymous functions and objects.

// Editor has no idea what to do
export default function(){}

// Editor will be able to auto import doSomething 
export default function doSomething(){}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aminnairi profile image
Amin • Edited

There is a clear difference between function expression and function declaration and they both serve a purpose.

This can't be a viable solution since imposing function declaration would mean losing the benefits of arrow functions that grab the outer context instead of creating a context. This can be destructive for some project relying on this mecanism.

Also, you can't use that reliably on scalar data types that uses default exports, it simply fails short on your text editor and its completion system (which comes most of the time from the language server). TypeScript/JavaScript language servers do not work well on default exporting scalar data types. So using a default export is very much problematic for a lot of things.

Instead, named exports are harmless in the sense that you can either named export a function expression as well as a function declaration, it does not impose on your team a way of doing things, plus the benefits mentioned above in this article. And it provides the best completions and auto-import capabilities from your language server when used in your text editor (and many editor benefit from that such as Emacs, Neovim, IntelliJ, VSCode, etc...).

Collapse
 
brense profile image
Rense Bakker

You can do this with arrow functions as well:

// Editor has no idea what to do and you should never do this.
export default () => {}

// Editor has no problem auto importing non-anonymous arrow function
const someArrowFunction = () => {}
export default someArrowFunction
Enter fullscreen mode Exit fullscreen mode
Collapse
 
chema profile image
José María CL

Yeah but that's a "if you setup that stuff correctly" 😕

Using named exports doesn't depend on ideal circumstances

Collapse
 
brense profile image
Rense Bakker

This works in vscode without any additional configuration and I'm pretty confident it works in most other editors as well, if they understand named exports they also understand default exports as long as your default exports are not anonymous functions.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Default exports can be confusing in complex modules. Developers may not know which export is the default or even be aware of its existence. This can cause delays as engineers must spend more time looking at documentation or even the source code to find the functions they need.

I've read this paragraph several times now and I still think it makes no sense whatsoever. Can you explain what you mean by this?

If you decide to rename a named export in a module, for example, changing the name of the add function to sum, most IDEs can automatically update all usages. This makes the refactoring process much easier.
However, with default exports, this isn't possible.

Again, this doesn't make any sense to me whatsoever; what is the problem here? Default exports have no name, so what would you want to happen here?

As we discussed earlier, when a module provides named exports, we can easily select a specific function from the module by using a shortcut provided by the IDE. This feature not only saves time, but also helps the IDE suggest and auto-import the necessary functions as we type.

This one makes more sense, but it's not really an argument against default imports. As long as your default export is also available as a named export (which is probably a good idea regardless), this still works 100%.

Default exports can be imported using any name you choose. This can lead to inconsistencies when multiple engineers are working on the same codebase and using different import names.

Again, this is a good reason to also have a named export for your default export, but it doesn't speak against having a default export in any way.

Setting that aside, regardless of consistency or not, if I look at a source file and see import add from "sum" I will probably scratch my head multiple times and tell whomever wrote that to fix it (or just not interact with the project if it's OSS)

Conclusion

Just provide named alternatives for your default export and everyone will be happy. We can have both.

Collapse
 
wakywayne profile image
wakywayne • Edited

Isn't that a bit repetitive though? Regarding both default and named exporting.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

it's three keywords per file, I don't think that's the kind of thing to seriously be worried about.

Collapse
 
butalin profile image
Anass Boutaline

This could be a discussion rather than a judgement, this way you're misleading others, named export or default export each export type has a purpose, some times you need to export a single function from a file to serve specific purpose, as you can use named export when you wants to group multiple functions in a single file etc

Collapse
 
htho profile image
Hauke T.

You had me at "Refactoring" - very true.

Collapse
 
seandinan profile image
Sean Dinan • Edited

Yep, spot on. Especially the Inconsistent Codebases section.

In the rare instances where a named export does need to be renamed, it can always be done with as:

import { Provider as RollbarProvider, ErrorBoundary } from '@rollbar/react';
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wakywayne profile image
wakywayne

I'd say what the OP is suggesting is probably the safer bet, but this really is only an issue in really big code bases with lots of people working on it. Also in frameworks like React that have strict file naming conventions as long as you follow those conventions, you should just use the previous standards not make up your own. Another situation where default exports are better is when you are making a npm package, in this use case it only makes things worse for the user if they have to remember the name of the export.

Collapse
 
ekdikeo profile image
Eric B

Most of these aren't real problems, and many people use default exports just fine.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

The IDE has no issues finding default exports while typing. It is a matter of providing proper information. Someone already said that if you export a nameless something, then that's a problem, but if you export something with a name, that name shows up in autocomplete.

Furthermore, NPM packages should provide proper d.ts files. Using my vite-plugin-single-spa package as example, you just install, then open vite.config.ts and you start typing "import vitep". At this point you should see autocomplete showing you the right option (vitePluginSingleSpa).

Collapse
 
drewkilleen profile image
Drew Killeen

Good thoughts, I like that you're challenging convention and asking us to think through things rather than just following the norms.

In my opinion, all of what you're saying is true when a file has multiple exports. I do find it confusing to have a default export mixed with named exports. But typically when a default export is used, I only see a single export from that file. I haven't run into any problems with that, especially since the file, function, and import almost always share the same name.

But at the same time, I also see no argument against using only named exports, so maybe you're on to something.

Collapse
 
jackmellis profile image
Jack

This is the developer-equivalence of first world problems.

IDEs have no problem discovering default exports in my experience. And I frequently rename default exports and vscode is smart enough to rename imports automatically.

I also have absolutely no issue with somebody deciding to only use named exports in their codebases, go for it. But here we're dressing personal preference as best practice which is incorrect.

Collapse
 
gibbitz profile image
Paul Fox

I have worked on code bases with default exports where casing and typos have caused discoverability issues.

import Buton from 'Button'
Enter fullscreen mode Exit fullscreen mode

If it can break "find in files" that's a good enough reason for me to not use it. IDEs make our lives easier in a lot of ways, but at the end of the day an IDE is just a tool. If you need it to understand your code, there's a problem. Anything that makes the code easier to understand without tooling is best practice in my book.

Default exports are not needed. Exporting a single named function from a module is completely unrestricted. If you need to rename a module, use the as directive.

import { module as betterNamedModule } from 'module';
Enter fullscreen mode Exit fullscreen mode

Then grepping the codebase will at least see the import reference. You won't even need it if you take your time namig things and refactor when things become so similar that their names overlap. So why use default exports?

Collapse
 
shawn_p profile image
Shawn P • Edited

The reason we have so many different tools at our disposal is so that we can use the best tool for a given context, and contexts vary greatly in this field. It is disappointing to see so much of these “never use X conventional method ever again” articles and videos which usually fail to sufficiently acknowledge the optimal use cases for the thing they are bashing on and seem aimed at being provocative and generating clicks more than offering genuine educational content. This article starts by acknowledging the correct use cases but then proceeds to suggest never using them ever again.

Default exports are absolutely the right choice when there’s only one thing to be exported, and there are plenty of architectural nuances from project-to-project that could warrant their use in other contexts. This point-blank statement of “default exports are bad and you shouldn’t use them” comes across as biased and imho could be potentially misleading to beginners.

Collapse
 
matveit profile image
Матвей Т

A thing I tend to do, when I am working on a module where it makes sense to have multiple exports, but also one main default export is this:

export function add(x: number, y: number): number {
    return x + y;
}
export default add;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jpilson profile image
Joao Sumbo (J.P) • Edited

To me , it seems like this articles seems like a personal issue l.
I use Vue , and Typescript , on top of that I apply OOP , which means there are a lot Class files that it makes sense that one File holds 1 default class and maybe some types. I use default export often an I have never any of the problems you mentioned my IDE (Webstorm, intelij) knows exactly where are they. On top of that I use the default export in conjunction with "named export" andy IDE k ow exactly how to import them . import A, {b}.

Collapse
 
dtasev profile image
Dimitar

If you're getting confused by exports then I'd recommend investing a few minutes in getting a free IDE.

As a newcommer to the JS env from cpp/python I don't really see the need of default exports. Everyone else has managed without them.

Collapse
 
aloisseckar profile image
Alois Sečkár

Good summary.

I especially dont like when I am forced to mix named and default imports from 3rd party libraries. (yes, I know I can make a custom reexport somewhere in codebase).