Hello humans. In JavaScript, the worlds most loved and internets favourite client-side language, thanks to modern ECMAScript standards, we have default and named exports.
It’s simple, and you have a file that exports something to be imported somewhere else. A named export is explicit and is only importable by its defined name. A default export is implicit, and you can import it and call it whatever you like.
Now, default exports came about in the CommonJS world of Node.js where you would import a module using const MyModule = require('my-module')
to account for uses where exports are default module.exports = MyClass
– although, it is worth pointing out that CommonJS does support named exports.
The most persuasive case for named exports
All modern code editors and IDE’s provide autocompletion functionality. If you are using Visual Studio Code (chances are, you are already), then you get some nifty auto-complete functionality out-of-the-box, even if you are not using a superset like TypeScript.
A default export receives no such auto-completion hints because it’s a default export, it could be anything; a class, a function, a constant. A named export explicitly tells your code editor what you’re exporting and importing.
Furthermore, default exports are difficult, if not, impossible for bundlers to tree-shake your code. A default export means that instead of just keeping the code you’re using, the entire file or in some cases an NPM Package is bundled into your code, and therefore adds bloat.
There are a plethora of other interesting issues that have arisen for people, further highlighting the reasons for avoiding default exports. Rich Harris succinctly worded it in his response to an issue on the Rollup repository on GitHub in 2016.
We absolutely would have been. Default exports have caused no end of problems. People get desperately confused by all the different forms of import/export declaration – imagine if we could teach people that you either import { names } or * as namespace, and that you can export either names or declarations. As it stands, it feels like there’s a ton of different variations you have to understand.
Plus the confusion that arises over whether default exports are live or not. I’ve spent more time learning about ES modules than anyone should reasonably be expected to, and I had no idea that the situation is as you’ve described. (Marked this issue as a bug, btw.)
And then there’s the interop headaches. Ostensibly, privileged default exports were meant to make adoption easier for a community that’s familiar with Node modules, which is ironic as nonsense likemodule.exports.default has probably caused more friction than any other aspect of ES modules. I’m sure we could have come up with a better way of importing single-export CommonJS modules. (Though we shouldn’t really call them CommonJS modules – CommonJS modules can only have named exports!)
Unfortunately, we’re stuck with it.
Default exports are lazy
There is no reason to use a default export unless you’re lazy and cannot be bothered taking the extra 5 seconds to add curly braces around your import and make sure your export is named.
There are exceptions when you’re dealing with a third-party package and have no control over how the exports are defined. However, even so, in that situation, a pull request on the repo for the library you’re using might be worth considering.
There is no legitimate reasoning for default exports, but there is plenty of legitimate reasoning against them. Make your life easier and avoid them altogether.
The whole imports thing is a complete nightmare. I’ve literally wasted afternoons flailing around trying to work out the correct import to go with the correct syntax in .d.ts for a third party library that isn’t already typescript friendly. “Desperately confused” is quite a fitting description!