Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Why we have banned default exports in JavaScript (neufund.org)
82 points by mmmnt on Nov 23, 2017 | hide | past | favorite | 72 comments


The historical claim, that default exports we're introduced for compatibility with CommonJS/Node, is not accurate.

Default exports were introduced because often a module wants to export a single value or piece of functionality. In such cases, in module systems where all exports are named, the author of the module and its consumer have to coordinate on some convention to indicate "this is the thing". For example, some languages choose the same as the file name (with fun casing consequences or conversions). Some choose a particular name like "t" (I've heard one of the MLs does this). Some don't have a convention at all and you have to consult the documentation for each import.

When designing JS modules, we decided to bake in a single somewhat-privileged export name for these cases, "default", which gets nice syntax on both the export and import sides to help encourage ecosystem standardization and coordination.

You can choose to deviate from it, in favor of your own convention. (It seems like the author prefers some kind of filename-converted-to-camelCase for their projects.) But do so being aware you're walking away from the ecosystem affordances and it will be unexpected for your consumers.


If default exports are meant to help export only a single value from a module, how come they allow also exporting other values alongside the default value?


Two of the three reasons listed involve refactoring and autocomplete quirks of Visual Studio Code, specifically. For instance, Webstorm has no such issues with default exports. It is smart enough to find all uses of a module and to track them down. The third reason is not all that common in my experience. Tree shaking is an optimization and often a premature one. It's also a technique primarily used for third party libraries.

My problem with this article is the advice is far too broad "ban all default exports" without being considerate of how others code.

For example, I prefer to keep my modules small, with only one export. I may export helper or unwrapped versions of higher order components for easier testing, but generally I want my modules small and single purpose.

Default exports are helpful constructs. Disagree with this article overall.


In webstorm/intellij say you have a react component, one export per module as you say. The import statements are a tie imo to write by hand:

  import MyThing from "../../components/editors/MyThing";
  import {MyThing} from "../../components/editors/MyThing";
But if it's named, you don't need to stop what you're doing, scroll to the top of the file, type out the import statement by hand, and look over at your code tree and figure out the path to the module you want.

You just type MyT Ctrl-Space and let intellij do all that for you, not even having to move out of the function you're writing.


You shouldn't code around your tools. Your tools are there to serve you, not the other way around. Also, devs on my team use vim, Sublime, Atom, VS Code, Webstorm/IntelliJ, and many more. They all work radically different for things like this.


You can easily add the import statement, named or not, with option-space


So it's exactly like vs code?


Agreed, I'd argue the developer experience is often worse using named exports. I can find myself wondering 'What names does this module export again?' and it often can encourage putting only tangentially related things in a module together.


Default exports also have some oddities involving live bindings, since the typical syntax is to export an expression, not a binding, but there are workarounds to make it a real live binding. Here's a GitHub discussion where some people who are pretty good with JavaScript are trying to figure out how to properly implement default exports: https://github.com/rollup/rollup/issues/1078

Particularly this comment, which agrees that default exports were a mistake: https://github.com/rollup/rollup/issues/1078#issuecomment-26...


I miss the simplicity of CommonJS.

> Ecma Script Modules which finally solved the problem of sharing code between files (modules) on a language level. It was a huge step forward

Step forward? We were writing modules just fine in 2010. I don’t know a single project off the top of my head that actually benefits from tree shaking. It has been 5-6 years since modules appeared and there is little to show for it. We could do almost the same with node’s commonjs. You could write code that would run anywhere without pre-processing or transpiling. Every time I see a project where the first three pages of documentation are about setting up Babel and webpack I feel like switching off.


I keep seeing this misconception repeated.

Both default and named exports can be rebound to new symbols locally. But that has no effect on how statically analyzable the code is.

“It can’t be auto refactored” just isn’t true. If it’s true for your particular tool, file a bug.

People are using a cargo cult understanding of how code analysis works. It’s not just grepping for a string.


I assure you that I know how static analysis work but how could you rename something that doesn't have any name? How could you perform "rename" refactoring on `export default function () {...}` since this expression is not associated with any symbol.

It's clear for me that in this case you could talk only on refactoring like "rename all default imports to:" This is different than simply renaming a symbol and I don't know IDE that supports this operation.


Your editor knows the symbol of every value referencing the default export. Try go-to-references on a default export.

If your IDE doesn't support rename-references that's your IDE's deficiency - not default exports.


Default exports don’t export any name ie. symbol that can be easily associated with a exported value. Named exports, on the other hand, are all about having a name (pretty obvious right ) . This makes possible for IDEs to find and import dependencies for you automatically, which is a huge productivity boost.

There's no reason a default export cannot have a name.

  export default function foo() {...}
With this in place VS Code is able to auto-import the dependency and refactor its name if necessary.


A default export is what a module is; a named export is what a module has.

Both tools are useful; both are necessary; most modules should have a single default export.


> Both tools are useful; both are necessary

Maybe a nitpick, but given that most other programming languages don't distinguish named vs default, it's probably not necessary. You can do all your programming where every module is something (like in Java) or where every module only has things (like in Python), but there's certainly value in being able to express one or the other in the same language. The downside is that the named/default distinction adds language complexity and learning curve, makes tooling support a little harder, and makes things like CommonJS interop more of a pain.


"Maybe a nitpick, but given that most other programming languages don't distinguish named vs default, it's probably not necessary"

Not sure that how other languages do things really plays a part in how JS does things?

I think his point is that having them available is necessary, because they both have strong use cases.


That's a good way of putting it. I often have modules that ARE one thing. By using the module, you're using the entirety of that one thing. For example, a JSX component.


I agree for different reasons. In our code we never export a single value without name because it's not consistent. Many of our modules export a single class with the same name as the module. One less thing to remember.

I dislike when I use some external module and I have to look up whether the module is the value or not...

Edit: as an additional annoyance, when mixing CommonJS and ES6, the value may be in an element called "default". In some cases. I'm still unsure when.


What I feel strange about default export is that you can actually mix it with named exports.

    export default class Foo { }
    export const Bar = "";
and then

    import Foo, { Bar } from "./module"
I would expect if a module has a default export, then it should not have named ones. Why? At least it puts more structure in your code architecture.


As someone who only occasionally uses JavaScript, module imports are really confusing. Often, I want to do

    import * as mymodule from './mymodule'
Namely, treat the whole module as one thing, and give it a name. It would be great if you could do that without specifying the name manually, e.g. like "import mymodule" in Python.

The other times, I do:

    import SingleClass from './singleclass'
    # or
    import {SingleClass} from './singleclass'
Its rather rare that I want a handfull of single things from a module - either one or all (all can mean "one object that has everything" or "all functions and constants in one wrapper", depending on how stuff is exported). The only time I need the `import {a,b,c} from './utils'` syntax is usually with a grab-bag util function module.

You get used to it, but I sometimes wish things were more like Python.


There is a reason for that, though.

There is no reason to import what you don't need. This is why wildcard imports are generally frowned upon in most development teams. (I say most, thats unqualified, of course, but from my experience, its always been a part of the code standards)

Your code is much more readable and clear when you are explicit about what you are importing.


"module.doSomething()" is more explicit than "doSomething()"


I can for my life not understand why the standards committee did not make the NodeJS module system the standard, what where they thinking !?


It's possible to statically analyze ESM — it's not so simple with nodejs modules.


IIRC there were some technical challenges to implement the same structure on the client side. Also, they wanted to make imports declarative.


Also the use case in Node of loading files from local disk is very different than the use case of loading scripts over HTTP in the browser.


Only this year the syntax for importing modules at runtime has been worked out. Current behaviour is exactly the same as commonjs and only useful for static analysis, 7 years later.


Because it's synchronous.


I'll preface this by saying I'm not a huge fan of ES2015 modules. Everything ES2015 provides (syntactical sugar) could be achieved with CommonJS exports. If you don't want to export an object and only a single thing, you can enforce those restrictions via code reviews, linting tools, etc. rather than bloating the language spec. And the backwards compatibility is very poor with older CommonJS (the weird __default stuff). This is what happens when you try to bolt on features from other languages that make no sense with javascript.

The next point is regarding IDE's. Honestly, I don't understand the obsession with designing a language around tooltips in an IDE. I personally use vim and once I know a code base I rarely need to look stuff up. If your code and internal APIs are that complicated where you can't remember basic function signatures, its time to refactor and simplify so you can fit all that stuff in your memory without the need for hints.

To the point about refactoring: I really doubt named imports are going to save you much time here at all. How much time do you spend figuring out the import/exports from a file vs. refactoring what the actual code is doing? Unless you're talking about some kind of formal AST automation (which could benefit from being more explicit)?

The next comment is about tree shaking. Better known as "dead code elimination" and its been in uglifiyjs and closure compiler for 10 years but somehow the webpack folks thought tree shaking sounded cooler, even though they don't even implement it and just shell out to uglifer. My argument here is that if your code is so messy and is in need of good shake (no pun intended, well...err), you have big problems. I cringe when I see people trying to layer on tools to paper over technical debt. You should actually go through the source and remove what isn't needed or used anymore.


Your comments don't scale well.

Projects have dependencies from teams with various skill levels and external dependencies over most of which you have little or no control. For this and many others reasons "refactoring to simplify" often isn't an option. Similarly, tree shaking doesn't just apply to the code from your project. Also, what about the time when you first start on a project? Or maybe you're only working on a project for a short time? Wouldn't it be nice if the experience was good regardless?

The author makes a good suggestion for consideration. Anytime we find a principle which is likely to lead to an overall improvement at scale it's worth considering, especially when it comes without much if any a downside. I'm not necessarily agreeing that this is the case here, but just don't think your comments add much.


If the source of the problems are external dependencies, which, as you stated, you lack control, how would you go about banning default exports in those libraries?

The correct and pragmatic course of action to address problems in code quality is to address them by refactoring, one file at a time. If you want to see the effects of explicit imports, go ahead and look at any large java code base. What ends up happening is fragmentation and enormous and tediously huge import declarations at the top of every file. Again, it's better to address the real problem and not the symptom of poor code quality.


Look I happen to agree about refactoring, but my point was that the authors reasons do matter in the context of an organization or the community, even if they don't matter to you as an individual. If there is a best practice here (which is far from decided from this article along) then it's a worthwhile point to bring up for discussion because while you can't control, you can hope to influence.

Also your point about tree-shaking is incorrect as it endeavors to apply to dependencies as well.

Finally, even without default exports one can still import the entire package with import *, so I don't see how it would lead to the same "problem" of a huge import declaration in Java.

In any case I _do_ agree that the authors suggestion should not be taken as a substitute for refactoring, but discussing this as a possible best practice is NOT mutually exclusive to this belief.


> I personally use vim and once I know a code base I rarely need to look stuff up.

How much code do you work with, out of curiosity? At my work, we have about 500,000 lines of code, and while I'm usually working on a specific part of that, modifying and interfacing with other parts of the code is common. And, of course, for new people and people switching teams, all code is unfamiliar. IMO, expecting people to "just know the code" is really unreasonable; working with code you're not familiar with is very common in software engineering, and our tools and practices should take that into consideration.


> I personally use vim and once I know a code base I rarely need to look stuff up.

Must be nice to work on stuff you're familiar with for years, day in and day out. That's not the use-case.


DCO is still important in libraries. If you have a large-ish library of mostly unrelated code (think lodash/underscore), DCO is pretty darn useful for the end users: you don't need to weigh in filesize (pun intended) as much when developing new features.


default exports are precisely as tree-shakeable/dead-code-eliminate-able as named exports; this topic just isn't remotely relevant to the discussion.


[flagged]


Would you mind commenting according to the guidelines instead of like this?

https://news.ycombinator.com/newsguidelines.html


The author here, it's great to finally make it to the front page, thanks! :D Initial inspiration for writing this post was twitter discussion: https://twitter.com/krzKaczor/status/933705625883889664


It would be helpful to link to something like this in case someone isn’t familiar with the difference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


How is this different than just relying on the filename to establish the naming contract for a default export?


I might take the opposite stance and ban destructed named imports, preferring default and wildcard imports.

    import function from 'function';  
    import * as module from 'module';  
Why wildcard imports over named imports?

Wildcards preserve context. `ReactDOM.render` means more than `render`. Using wildcards avoids collisions - lodash, http, https each have a named export `get` [0]. Wildcards still supports tree shaking! Try it in webpack or rollup - only the named exports that you access will be included in the shooken bundle [1].

[0]: Yeah, I know you can import { get as httpsGet }, but why would I want to?

[1]: Unless you treat the binding as an object. `Object.keys(module)` will break tree-shaking.


Sometimes you don't care what the importing module names your thing, and it's the only thing your module does. Think of a super simple React component... I'm `export default function(props) { ... }`ing that every time. Is that bad?


AFAIK it's bad practise to export components like that, as they won't have a displayable name in devtools etc.


Pure/function-only components will not have a name anyway


I think they get their display name from the variable name


    export default function MyComponent(props) { };


This is not a bad practice

  export default class C extends React.Component { ... }


I use TypeScript and VSCode and recognise the easier refactoring, but almost all external dependencies will use default exports, so you just end up with an inconsistent code base.


In TS most of my export imports look like import * as something from "something";

which makes me cry :D


I'd ban default exports if and only if JS had a Java-like import syntax declaration.

   import {x,y,z} from 'module'
^ this is an abomination


Yes,

    from 'module' import {x,y,z}
would be nicer


No, this is far preferable to human beings who speak English:

    import {x,y,z} from 'module'
Yoda from Star Wars might agree with your preference since he speaks backwards English.

All kidding aside, I'd love to hear your logic about why you think the backwards version is better.


I commented below, but I'll comment here as well:

    import module.x
IMO is much better:

- It goes from "larger to smaller". You parse the line with your eyes and immediately see where things are coming from. With `import {x} from module` you have to do a double-take on what comes from where.

- If you use any sort of code assist, you get to autocomplete immediately when you hit `.` With Javascript's weird thing you have to first type `import {} from module` and then go back to {} to invoke the autocomplete. This is especially infuriating when you don't really remember the name of the thing you're importing.


Even PHP's approach makes sense compared to Javascript's:

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
    use Symfony\Component\Form\Extension\Core\Type\DateType;
    use Symfony\Component\Form\Extension\Core\Type\EmailType;


PHP doesn't import anything this way though, it just makes the code aware of namespaces. The actual import is done with spl_autoload_register .


OSV grammar is legitimate English.


when I write the module before the parts, the autocomplete works better


Or import module.x

etc.


Semantic difference between default and named exports is that as a user of the module you are forced to choose a name for the default export, and these names will likely be different across different usages of the module. For linking units within one project, this is a bad thing, because you should be consistent in naming your things. But for modules that are published to the global NPM, maybe why not.


1) you should be consistent, it's not your place to force users of your code what to name things 2) `import { foo as myBetterName } from 'path'`; your argument (about consistent names) is invalid :-p


If you import stuff from many libs, chances are they're named the same and you have to rename them anyway


Thanks for writing this up, I had similar feelings about default exports and there are very few articles that discuss this. Many examples out there use default exports and it's just bad form to export in that form when there are classes functions and variables that might be referenced directly.


Surely there are drawbacks in refactoring, as unless your editor supports bulk renaming references you will have to rename every single import and usage. Whereas with a default export you have no such issue.


While reading this thread away from my machine, I'm curious now what happens when

export default { foo: 2 };

export const foo = 3;

// other file

import { foo } from './file'

console.log(foo);

Presumably last one wins or, if the other way around, a reassignment of const error?


export default { foo: 2 };

is not the same as:

module.exports = { foo: 2 };

The way your example would translate to commonjs is:

module.exports = { default: { foo: 2 }, foo: 3 }


3 is logged into the console without any warning / error.


Thanks


When I shake the tree all the leaves stay on, so default exports are probably fine.


If you use an IDE like Visual Studio Code or Webstorm, and not a text editor like Sublime, then it autocompletes default exports perfectly fine. Just stop writing code in a text editor like Sublime/Vim/Emacs/Atom/etc.


They could have a `private` keyword like Java.


Well, the most widely used style guide [0] and Facebook and many others including myself disagree. I'd rather see default exports everywhere so I only ever have to import one thing from your file.

None of the points made in the article are even valid.

1) You don't get better DX because you simply cannot use the same symbol name everywhere all the time anyway because names clash.

2) With point 1 gone, point 2 is wiped out as well.

3) This is patently false.

[0] https://github.com/airbnb/javascript#modules--prefer-default...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: