Javascript: What is the benefit of prefer-default-export?

Created on 10 Apr 2017  ·  167Comments  ·  Source: airbnb/javascript

The docs don't have a Why section for prefer-default-export, and I'm not seeing the benefit of it on my own. I would think that _not_ using default is _preferred_. With default exports, you lose refactoring power (if you rename the source const/function/class, it won't rename default imports).

As more of an edge case: it makes code less future-proof. i.e. if you create a file that will be a collection of errors, but it only starts with one error, to follow the linting rules, you'll have to have it export default, but then when you add the 2nd error at a later time, you'll have to do a bunch of refactoring that could have been prevented if the recommendation was to avoid default export.

question

Most helpful comment

For your edge case: that can totally happen, but that's the case for an eslint override comment, which is an explicit indicator that this single-export file is intended to be a multiple-export file.

Given that - that you'd basically never have to change a default export to a named export - the refactoring power is all in the filename. Meaning, you a) change the filename and rename all the imports (importjs can do this for you; eslint-plugin-import will ensure you didn't miss any); b) renaming any of the code in the file does not change how consumers import it; whereas with named exports, the name in the code is tightly coupled to the name that it's being imported by; c) this rule encourages more files that only ever export one thing, as the default, which is better for readability, maintainability, treeshaking, conceptual understanding, etc.

All 167 comments

For your edge case: that can totally happen, but that's the case for an eslint override comment, which is an explicit indicator that this single-export file is intended to be a multiple-export file.

Given that - that you'd basically never have to change a default export to a named export - the refactoring power is all in the filename. Meaning, you a) change the filename and rename all the imports (importjs can do this for you; eslint-plugin-import will ensure you didn't miss any); b) renaming any of the code in the file does not change how consumers import it; whereas with named exports, the name in the code is tightly coupled to the name that it's being imported by; c) this rule encourages more files that only ever export one thing, as the default, which is better for readability, maintainability, treeshaking, conceptual understanding, etc.

this rule encourages more files that only ever export one thing, which is better for readability, maintainability

@ljharb That sounds like a good summary for the readme. It might also help to clarify that the rule only warns about files that are exporting one thing.

@deckar01 a PR to add it would be quite welcome! :-D

this rule encourages more files that only ever export one thing, as the default, which is better for readability, maintainability, treeshaking

Agreed except for treeshaking. All else equal, a single export fights against tree shaking by importing all code when you may only need some. Smaller discrete exports aid tree shaking.

@coryhouse This rule only applies to files that export one thing. If a file exports multiple things, then it will not complain about not exporting a default. Still not sure it makes any difference for tree shaking though.

@coryhouse no, it doesn't. 3 files each with 1 export, versus 1 file with 3 exports, is identically treeshakeable - except that the former doesn't need tree-shaking to be as small as possible.

Using named exports is why tree-shaking is even necessary in the first place.

It's sadly a style I've seen too much to export an object with all of the "named" exports as members. This then breaks tree-shaking (for obvious reasons), where as you can tree-shake if you export them all separately.

@aboyton Indeed, this is also true - and default-exporting an object that's really just a bag of named exports is both a) conceptually the same as named exports, and b) objectively worse by all metrics, including tree-shake-ability.

The ideal module/file only default-exports one thing, ideally a pure function. There are always exceptions and justifications for deviating from this, of course - but that's the default I start from.

@sibelius that article is full of claims about named exports that apply identically to default exports; it's just not convincing reasoning.

@ljharb we're struggling with this: let's say we have 500 helper functions related to UI manipulation (hide/show elements, rotate, fade in, etc etc). What's best for the long term? 500 individual files each exporting one function seems overboard, but maybe not? Currently we use a few revealing modules that group semi-related methods. Refactoring to es6+ I'm just not sure how best to handle this scenario. I've talked to a few folks with similar scenarios and we get into a seemingly circular argument "well airbnb pushes for a single default export...but tree-shaking works with multiple named exports just as well...what's really better though 500 files or 500 named exports..."

what's really better though 500 files or 500 named exports...

Those are the two most extreme solutions and neither of them are good. A more practical solution is to export 5-20 functions per module. That makes the files easy to edit and possible to tree-shake.

@KayakinKoder 1000%, if you have 500 helper functions, have 500 individual files. Use directories if they need organization. Tree-shaking is a tool to help fix a mistake you made - it only has any impact in the first place when you screwed up by importing more things than you needed, from modules that exported more than one thing.

What about the refactoring argument?

Default exports make large-scale refactoring impossible since each importing site can name default import differently (including typos).

// in file exports.js
export default function () {...}
// in file import1.js
import doSomething from "./exports.js"
// in file import2.js
import doSmth from "./exports.js"

@ha404 it's a ridiculous argument, because you can rename named imports as you bring them in, and you can typo those too. Refactoring is identically easy with default and named exports with respect to the names of the imported variable. Separately, it's a feature that with default exports, everyone can more easily import it as their own name. If you want consistency with import names in your project, use a linter to enforce that - don't change your module architecture to try to enforce that, especially when you can't even force it that way.

Well if you were to rename a named export wouldn't you do:

// somewhere.js
export const specificNamedFunction = () => { ... }
import { specificNamedFunction as blah } from 'somewhere'; // must be explicit when renaming
import { anythingElse } from 'somewhere'; // this will break



md5-dba0ab258b09dce4fe0f42c74765af84



```javascript
import blah from 'somewhere';
import whoop from 'somewhere';
import anythingInTheWorld from 'somewhere';

@ha404 sure. so you've made it ever so slightly harder to rename it by using a named import; but the important factor isn't "how hard is it", it's "is it possible at all, or not". If it's possible, then you've not prevented anything, so it's a non-argument.

The proper answer here remains to use a linter - like one based on this config - to enforce consistency within your project, and not attempt to do a half-baked job of it using architecture choices.

You ignored the "argument" for preventing typos and changed it to an "argument" about possibilities. People can feel free to go the extra mile to make a typo if they want, I'm not going to stop them.

It's ironic you argue consistency when having default exports makes your project less consistent. In your project, you'll have to switch between export default and export namedStuff depending if you have one or more exports vs. always using export namedStuff.

Maybe, I'm wrong, maybe you religiously export a single thing per file in your _entire_ project. I would hate to use Redux and have a single action per file...kill me 💀

This is all about possibilities; if it were possible as a module author to force the user to name the identifier a certain way, we'd be having a different discussion. It's not possible, thus, it's not a relevant argument to module authoring.

Regarding "single thing per file in your entire project", I certainly try to do that; of course, there's always exceptions (like action creator files, or constants files).

"Consistency" in a codebase doesn't mean "you only do one kind of thing". It means that when you do a thing, you do it in the same kind of way. Default exports and named exports are both tools the language provides; most of the time, a single default export per file is (by a long shot) the best thing a module should provide. Occasionally, named exports are needed.

Limiting yourself to only named exports would be just as limiting as limiting yourself to only default exports. Both have value, both are necessary. The article you quoted contains a number of arguments allegedly for "only use named exports" that are all fallacious.

I only quoted the refactoring argument. I never chose a side for ONLY having either or, there's always an argument for both cases. You just never addressed the typo concern, all I read about was possibilities and consistency. Is there a linter for typos?

Absolutely! This one :-) it uses eslint-plugin-import which verifies imports and exports across files. Thus, typos are a non-issue with either flavor of exports, when using this linter config.

Cool, that sounds like what I'm looking for, but I think you're missing a link haha

No link is required; use this repo's config and you get that checking for free, that's part of the whole point of it :-p

Limiting yourself to only named exports would be just as limiting as limiting yourself to only named exports.

I presume one of these occurrences were meant to be "... to only default exports"? (So as to be less tautological, unless I'm missing something here)

Although this rule encourages _single_ default exports, it does not discourage _multiple_ named exports.

  • If you are exporting one thing and forget to make it the default, the rule just reminds you to make it the default. This is is the primary use case.
  • If you have one thing and you know you are going to add more stuff later, add a linter directive to the file temporarily to silence the rule. This is an exception to the rule.
  • If this rule conflicts with your project's existing style, just turn the rule off.

The strongest argument against the rule I have read in this thread is that, in the case of an exception, the developer might think they need to export a dictionary as the default. If that is the case and it is an undesirable style, then there should be a rule that discourages default dictionary exports.

@FireyFly yes thanks, i've corrected the typo.

As the one that complained that I've seen too many people exporting a dictionary as the default I'd love a lint rule that forbid people from doing this.

@aboyton there's https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-anonymous-default-export.md which only allows it if it's stored in a named var (which is weird), but otherwise I don't see anything. Feel free to file a proposal issue, or a PR, on eslint-plugin-import, and we can discuss it there.

For cases like this,
/ACollectionOfTinyComponent/index.js

import Container from './Container';
import Container2 from './Container2';
export {
  Container,
Container2,
};

So the index.js becomes a directory to include other small parts without having to write each Component Name out all the time.
So in this case, we can import a component in another component like following:

import {Container} from './ACollectionOfTinyComponent'

//then use Container in the code here

Is this a bad practice?

Whoever interested can answer here as well ...https://stackoverflow.com/questions/47874818/how-to-get-around-airbnb-eslint-import-prefer-default-export-rule-when-using-ind

^^^ I answered question myself, I think its just that by looking at the lint rule name itself is not that obvious for what the error was.

@adamchenwei if you're going to have a "manifest" file like that; named exports are the best way to do it - do NOT default-export an object.

However, I'd discourage such index manifests - all they do is make treeshaking necessary by importing too much into the dependency graph. People should deep-import the things they need from a file that default-exports just that thing.

My IDE can't and I think no IDE can resolve and automatically import the package I need as export default cut the relationship.

If your IDE works with named exports but breaks on default exports, I’d find a better IDE. There is zero reason default or named exports should differ in functionality or restrictability in any way, full stop.

Or maybe file a bug report with the IDE/plugin that is missing this feature. If it really supports named imports (which it might not), then it should support default imports. Naive autocomplete implementations using variable name matching could make it seem like it understands imports when it doesn’t.

@tom10271 It doesn't even make sense why an IDE would do better with named exports and not default ones. In order to find named exports it needs to index and parse every single file in the entire project. In the most common case for default exports you're going to name the import the same as end of the full path without extension.

So for an IDE when you type import map it could easily just suggest these: [import map from "./operators/sync/map.js", import map from "./operators/async/map.js"] or whatever files ending in map.js you might have.

Now obviously it's a bit more complicated if you wanted to import both the sync and async versions for example, but I don't see how named exports would make this much better, instead your IDE could just have some fancy syntax that is replaced (e.g. import async/map could suggest an expansion like import asyncMap from "./operators/async/map.js" or something like that).


Unrelated to the first part I'm personally not entirely opposed to named exports, but I think it would be better if tree-shaking could be opt-ed into by the language itself instead of as a tooling step that has to try to detect if exports are pure or not (which is hard!). I suggested a scheme for tree-shaken library exports decided by the library author here as static export ... from ..., but it didn't garner much interest.

I don't get the "single thing per file in your entire project" thing. Then why on earth is a JS file equal to a JS Module? A module with only one thing in it breaks the concept of being a module. A module is meant to group things that belong together, so named exports are more logical towards the fact that a JS file is a module.

I must say though that for libraries, I do tend to split everything up, just for easier development and better understand all the pieces of the library, but they become sub-modules then.

@dietergeerts Try not to mix up the concept of modules with the concept of packages. Normally when people talk about using modules to break their code up they mean it in the literal english definition of small parts that can be easily replaced and re-used.

The idea of a module is definitely not to group things that belong together, modules should be minimal pieces that be put together to build larger things from them. This is why it's recommended to split at the most granular level of only using export default as it forces you to make all parts relatively re-usable and replaceable.

Packages on the other hand are a group of things that belong together. In fact typically a package will be a group of one or more modules. You might even call a package itself a "module" as it's a replaceable/re-usable singular unit that be put into larger systems.

As an example consider a package like Angular, this package consists of many many modules (the small parts which make up angular itself). You might however treat the Angular package as a "module" because it's sort've it's own unit that you don't need to look into and can replace (normally just with newer versions) and re-use as you need.


Also don't confuse the authoring format with the concept, there's many authoring formats for modules (ES Modules, CommonJS, UMD, ...) but the concept of modules being small replaceable/re-usable chunks of the application always holds regardless of what format it's in.

@Jamesernator, I get your point, but is 20 files of all one line of code is easier to read through than one file of 20 lines of code? and where all these 20 functions all belong together very tightly.

This rule only applies to files that export one thing. If a file exports multiple things, then it will not complain about not exporting a default.

https://github.com/airbnb/javascript/issues/1365#issuecomment-346976818

@deckar01 , true, but that's the thing not wanted, as later on, we can add extra exports etc... An application is in constant development...

@dietergeerts https://github.com/airbnb/javascript/issues/1365#issuecomment-394702944 is spot on; a module is not necessarily meant to group things that belong together; that's what directories are for. A module is meant to encapsulate an explicit API - whether that's one thing or multiple things.

One of the strongest arguments _against_ default exports, which was mentioned above but seems to have got a bit lost in the noise, is that renaming named exports is much more explicit than renaming a default. Which means you can definitively find all imports with a simple grep for ExportedName as. With default exports it's much harder to find all usages.

This alone is a big enough reason to ban default exports for me.

@gregplaysguitar i responded to that right below it: https://github.com/airbnb/javascript/issues/1365#issuecomment-351476589

With either case, you'd want to use a tool that understands export specifiers, because of export from declarations - so that's not a reason to ban default exports either. Regardless, renaming is fine.

If for whatever reason you don't want the consumer to change the "name" of your "API", then named exports are not much better than default exports. Export a class for that, so that the class methods become the "API" (again, doesn't matter if that export itself is default or named).

This has nothing to do with linters, or export from, or exporting classes.

Consider two scenarios

export default MyThing

and

export MyThing

In the former, to find all usages I need to grep for imports by _import path_ which varies with relative imports, so isn't consistent. Then I have to scan the imports to see what the imported names are, then I have to look for the imported names to see where they're used.

In the latter it's as simple as grep -r MyThing ./, and I can see all the usages (and maybe _explicit_ renames) in one list.

(This is also the very reason why it's much easier for "Find references" tools like flow's to find usages of named exports)

@gregplaysguitar "find references" tools build a dependency graph, and then use specifiers - they don't do grepping, so that's irrelevant. Can you elaborate on your use cases where you want to find all usages, and why, and how using eslint-plugin-import as defined in the airbnb config won't report on those if they're no longer valid?

I'm working with a large legacy codebase where we frequently refactor old code, and need to understand where and how it's used in order to facilitate that. The prevailing pattern is default exports and for a few reasons our IDE setup doesn't just find all of these for for us; in any case grep is simpler and my position is that default exports make grokking the code much more difficult due to what I outlined above. So going forward we're preferring non-default exports.

All of this isn't meant to be an argument that you or others should change your practice, it just seems to me like "grepping for references" is a valid advantage of named exports which hasn't really been acknowledged. Dependency graphs are great, but I don't want to depend on a complex tool if a simple one can mostly do the job.

How is grepping the last part of the import specifier (e.g. someModule.js in /path/to/someModule.js) any worse than grepping the imported name? It seems like grepping for someModule.js is guaranteed to find all files that actually import someModule.js.

(Sure you might have multiple files called someModule.js but then you'd have multiple import { someModule } as well so I feel like that's moot).

That's a fair point, and perhaps file-naming consistency is part of our issue here too, but you're still left with the additional step of searching for usage after you've found the imports. Really I think it boils down to the merits of renaming, which default exports encourages. I'd prefer a given thing to keep the same name throughout the codebase. But each to their own.

@gregplaysguitar to be honest, i agree with you - but the way to enforce that is with a linter rule, one that can catch both default import names and named import renames, not by avoiding one of the critical pieces of ES Modules: the default export, which is what the module is (named exports being what the module has).

Since avoiding defaults can't guarantee "the same name throughout the codebase", while a linter rule can whether you use defaults or not, then avoiding defaults for that reason makes no sense.

Since avoiding defaults can't guarantee "the same name throughout the codebase", while a linter rule can whether you use defaults or not, then avoiding defaults for that reason makes no sense.

But if the goal is name parity, named exports _does_ make more sense... if you misspell, or miss one in a refactor, the code won't work and you'd catch it quickly. Anything above a very basic text editor will catch an error like that out of the box.

To enforce a name change with defaults, it is more work. Gotta set up a linter, make sure people lint, or maybe CI does, etc. One is a matter of semantics, the other is not.

I'm the one that opened this issue, but my concern was just wanting it to be documented. I don't agree with it, but at least you all have your reasoning, and it's now documented (tho again, I think the argument in the official documentation is weak!). We all of course know that we can just change the setting, but I do think we (as JS devs) should advocate for named exports, however, it's _clearly_ very subjective.

But I do enjoy this thread getting resurrected from time to time. 😄

For those who prefer switching it off, you can edit your .eslintrc.json

{
...
    "extends": "airbnb-base",
    "rules": {
        "prefer-default-export": ["warn"],
    },
...
}

Quien eres

noreply

Talking about refactoring (which has been often mentioned as reason for avoiding default exports) I don't think it makes things more difficult if you use default exports.

You can still look for the path if you wanna see where an export has been used.
If a module has just one reasonable export (e.g. a class) I wouldn't avoid a default export just because it's 'bad'.

@dandv that article has a number of points that don't hold up to scrutiny:

If you refactor Foo in foo.ts it will not rename it in bar.ts.

this is true of named exports as well, hence, irrelevant.

If you end up needing to export more stuff from foo.ts (which is what many of your files will have) then you have to juggle the import syntax.

this is also true of named exports - adding , { bar } to import foo is no harder than adding , bar to import { foo }.

Discoverability is very poor for default exports. You cannot explore a module with intellisense to see if it has a default export or not.
You even get autocomplete

This means intellisense is broken, not default exports. A competent IDE understands what is at this point a 3+ year old standard.

Better commonJS experience.

This is just nonsense; a CJS module only has a default export conceptually, despite babel's interop, and the discussion is about authoring a module, not consuming one.

You don't get typos

That's not a typo, that's just people naming variables differently, which is a) fine, and b) can be enforced by a linter if that's something you want to do.

Auto import quickfix works better

Tools like importjs work identically with named and default imports, so this is just false on its face.

Re-exporting is unnecessarily hard

I wouldn't recommend re-exporting in the first place, but export { default } from 'foo' as an extra step really doesn't seem that hard to me - and I'd actually say it's better to be explicit than to use export *.

@ljharb: thank you for addressing those. How about Dynamic imports() forcing access to properties via .default?

@dandv you have to specify a name with everything in import() - but if you author the module, you can easily do this:

export default Foo;
export function then(resolve) { return resolve(Foo); }

and then you get the default export directly from import(). Next?

this is true of named exports as well, hence, irrelevant.

Not true.

Default

export

export default const foo = 123;

use

import foo from './foo';

Rename foo => bar in export will not rename use.

Named

export

export const foo = 123;

use

import { foo } from './foo';

Rename foo => bar in export will rename use :rose:

@ljharb that example is from the document originally shared. Restored comment at your request ^ :rose:

this is also true of named exports - adding , { bar } to import foo is no harder than adding , bar to import { foo }

Not harder in terms of character count. But harder in terms of concept overhead. Why is one a default and the other named when both could be named. What about when you have three items, which one gets to be the default? Not worth the extra concept overhead. :rose:

Discoverability is very poor for default exports. You cannot explore a module with intellisense to see if it has a default export or not.

Here import { /* */ } from 'something' you get autocomplete. Here import /**/ from 'something' you get no autocomplete. Perhaps it has a default export / perhaps it doesn't. Autocomplete doesn't make choices on what you want to call something only on what is available :rose:

If it is still unclear:

const /* no autocomplete */ = {foo:123};
const { /* you get autocomplete */ } = {foo:123};

Not an IDE weakness. Syntax weakness :rose:

This is just nonsense; a CJS module only has a default export conceptually, despite babel's interop, and the discussion is about authoring a module, not consuming on

You might not have felt that pain. I and many people have :rose:

That's not a typo, that's just people naming variables differently, which is a) fine, and b) can be enforced by a linter if that's something you want to do.

I don't like forcing people to rethinking an already named variable. A variable is a concept that should be noun-infied and called the same thing consistently :rose:

Tools like importjs work identically with named and default imports, so this is just false on its face.

Not TypeScript. My notes are on TypeScript. Although a quick search on import-js does reveal : https://github.com/Galooshi/import-js/issues/453. That said first time I am hearing of importjs I am assuming it is import-js (something I just found from a google search :man_shrugging: ) :rose:

I wouldn't recommend re-exporting in the first place, but export { default } from 'foo'

When you create a module library you need to :rose:

but export { default } from 'foo' as an extra step really doesn't seem that hard to me

You cannot have two of those in the same file. The most common use of re-exporting is to roll multiple files in one :rose:

@ljharb Alright, that answers all the points you have raised. I've replied separately so you can discuss separately :heart:

@basarat The const /* no autocomplete */ = { foo: 123 } example doesn't really correspond with default exports at all as export default is explicitly defined within modules whereas objects may be anonymous structures.

Note that export default const x = 12 isn't valid, export default takes an expression (although there's some special-casing for functions/classes to also declare them in the file). The only real name you can give to a default export is the filename itself.

If an editor doesn't support renaming exports from import myFunction from './path/to/myFunction.js' to import differentFunction from './path/to/differentFunction.js' then that editor should support that. It's as easy to write as any of the named export ones so why the editors haven't done so is beyond me.

I love how the battle still ensues. In all seriousness though, what kind of person do you have to be to intentionally assign different names when importing your modules? Stick to the same name, and if needed, don't export anonymous functions or values - declare them with a meaningful identifier, export default at the end of file, and import using that same identifier. If you need to change the name globally, it's as easy as Ctrl+Shift+H. I find it foolish to not use certain features of JS because of fashion or danger. Why not leverage both with sensible discretion?

@alex996 One example might be if you have operators that differ per thing they operate on e.g.:

import mapObservable from 'rxjs/operators/map.js'
import mapIterable from 'some-itertools/map.js'

Another might be common convention e.g.:

import $ from './jquery.js'

Sure it's not super common, but renaming does happen intentionally and there's no reason to assume it's a mistake. Better than an editor tool would just be code reviews that ensure people are doing things sensibly regardless of whether they're renaming default exports or not.

Regarding https://github.com/airbnb/javascript/issues/1365#issuecomment-415236807: conceptually, default exports are what a module is, named exports are what a module has. This is a core language concept.

Re https://github.com/airbnb/javascript/issues/1365#issuecomment-415237317: have you used importjs? It in fact works with both, just as you'd expect.

Re https://github.com/airbnb/javascript/issues/1365#issuecomment-415237619: then presumably you never allow anyone to name their own variables so you can think for them? Local bindings are chosen for the convenience of the author.

Re https://github.com/airbnb/javascript/issues/1365#issuecomment-415238114, importjs works fine with typescript as far as i know; but the existence of bugs doesn't change that this is a tooling concern, not a code authoring concern. If your tools' lack of capability is forcing you to write code differently, that's a reason not to use that tool.

Re https://github.com/airbnb/javascript/issues/1365#issuecomment-415238455, no, you do not. Peo-ple can (and should) deep import from specific paths, not lazily import everything from a manifest, forcing reliance on treeshaking to partially clean up after yourself. Separately, you can export { default as foo } from 'path', so I'm not sure what you're referring to.

@basarat Also, please don't comment multiple times - each time generating email spam for hundreds of people, each time with a somewhat passive aggressive emoji at the end. Let's have a productive discussion, or none at all.

@alex996 "what kind of a person do you have to be" is needlessly insulting; please refrain from such language. I do this all the time - I name things based on how i intend to use them in the module, not based on what the author of that code arbitrarily chose. Often, these are the same, but that doesn't mean that my intuition must match the author's.

Why not leverage both with sensible discretion?

Absolutely I agree with this. I'll repeat again, in bold for emphasis:
A default export is what a module is, named exports are what a module has.
It is a mistake to discard a core language concept and a powerful means of expressiveness, especially because of unwarranted fears and inaccurate information.

if the tone on this thread gets toxic, i will lock it. I'd prefer not to do that.

@ljharb

This means intellisense is broken, not default exports. A competent IDE understands what is at this point a 3+ year old standard.

Then please tell me which IDE's do understand this...

@dietergeerts I'm afraid I can't, as I prefer an editor to an IDE. However, there's a bunch of conversation farther upthread - in which you were directly involved - that addresses why it shouldn't be any different for named vs default exports.

@ljharb I get the why and when of default exports, and the concepts. This is the only rule I currently overwrite because both Visual Studio Code and WebStorm/IntelliJ IDEA can't auto-import things with it, and the auto-import gives us a massive productivity boost.

For packages with default export, I create live templates, so I can import them fast and easily, like for Lodash and RxJS, which have exports named the same ;), and that's where the default exports of Lodash are really great, so I can get:

import _map from 'lodash/map';
import { map } from 'rxjs/operators/map';

So not against default exports, it's really useful in libraries like Lodash, but for most projects, it's better not to use them due to the productivity boost and developer experience.

I've subscribed to this thread so that I can discover and understand scenarios where named exports were found to be better than default, and vice-versa. Hope we can maintain that spirit in this thread.

Any suggestion that one approach should never be used has my automatic downvote.
Maybe you didn't recognize its use case or value yet, or using the other approach has so far been sufficient for your team. But before banning it altogether, maybe you should consider that language designers have probably thought a lot about it too, perhaps more than you, and included it for good reason.

If you go by the original design goals, default exports were intended to be the "favored" approach. The whole thing was discussed in depth before finalizing the ES6 modules spec. Note the point (from linked discussion) relevant to this topic:

The syntax should still favor default import.
ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.

My thumb rule based on a few years of using ES6 modules is that I'd mostly prefer default for exporting classes, and named for exporting a bunch of related functions/constants (instead of a class full of static methods). But I always decide afresh, even if it usually takes only a few seconds, what makes sense for the module that I'm about to create.

As far as the prefer-default-export rule is concerned, I have kept it for now, to serve as a reminder. And I don't mind disabling it inline where I am sure a module should have named exports starting with only one today.

It's sadly a style I've seen too much to export an object with all of the "named" exports as members. This then breaks tree-shaking (for obvious reasons), where as you can tree-shake if you export them all separately.

Isn't this the problem? If developers are not aware of how treeshaking works, the temptation to resolve the lint warning when exporting multiple functions from within one file is to export a single object with all the named exports as members. This makes the linting error go away, but it prevents treeshaking, so it's actually encouraging a practice that could lead to poorer performance. Surely the treeshaking argument alone is enough to encourage all exports to be named exports?

if the tone on this thread gets toxic, i will lock it. I'd prefer not to do that.

I read it - if the arguments are against mine, I will lock the thread. Everyone is saying "hey it's a bad idea" and you keep repeating "lalalala you're all stupid and my idea is better because it's mine." And what I infer is - "we can't change the rule because the entire codebase is already messed up and fixing it would require no less than 365 man days."

@chojrak11 not at all; there's tons of arguments against our position on the thread already.

We could change the rule and codemod it in about 5 minutes - that's not the issue at all. It's a worse mental/conceptual model and would make maintaining and reasoning about our code harder - it's not that it's hard to achieve.

a worse mental/conceptual model

This is why I wrote what I wrote. Saying "worse" is just your opinion, which is supported by theories that most of the people chose not to acknowledge. I'm glad I don't have to follow these rules, as I think the default export is _worse mental/conceptual model_ rooted in one-class-per-file philosophy from Java, which I also consider a failure.

@ljharb

@ljharb I get the why and when of default exports, and the concepts. This is the only rule I currently overwrite because both Visual Studio Code and WebStorm/IntelliJ IDEA can't auto-import things with it, and the auto-import gives us a massive productivity boost.

Created feature request for such autocomplete.
You can support it below with thumb up
Autocomplete of export default

Today I read an article that sums up nice why I still overwrite this rule, while in our team, the decision is to ONLY overwrite Airbnb rules when there is a clear benefit for dev and avoiding bugs.
https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/

@dietergeerts that article has similar flaws as similar arguments in the past: “names should be consistent through all files” can’t be enforced with named exports, and can be enforced with a linter rule on defaults; you have to look at a readme or code to know what names are available and what they do (“a side trip”) with names just like you do with defaults; if you want to find where a module is used, don’t use grep, use a dependency graph tool (like eslint itself). It’s unfortunate that people will likely be unduly swayed by the reputation of that post’s author :-/

I don't know the author, I just posted this here because it has some valid arguments. Arguments that are shared by a lot of people. Arguments I already had by myself, not because others have said so. Mine main concern is that named export are better for naming consistency and IDE support.

named export are better for naming consistency

This is unfortunately incorrect, because they can be renamed - the only way to enforce "consistency" is with a linter rule, which would be able to support defaults identically as well as names.

IDE support

If there's an IDE that supports defaults less well than names, please file a bug on it (if you link it here, I'll help advocate for it). There's no technical reason why they shouldn't be equal.

This is unfortunately incorrect, because they can be renamed - the only way to enforce "consistency" is with a linter rule, which would be able to support defaults identically as well as names.

Yes, correct, you can rename them with an as statement, but most devs don't do that unless there's a naming conflict. Using WebStorm's auto-import also doesn't promote this.

Sometimes it's not about black or white, as most things are gray. It's the intention and ease to be more aligned to one side that's more important. Not everything can be enforced, and not everything should be for that matter.

I'm staunchly in favor of enforcing anything that can be enforced, but I agree with you that most things are gray :-) Thanks for your thoughts.

@ljharb

@KayakinKoder 1000%, if you have 500 helper functions, have 500 individual files. Use directories if they need organization. Tree-shaking is a tool to help fix a mistake you made - it only has any impact in the first place when you screwed up by importing more things than you needed, from modules that exported more than one thing.

I think some of the hesitancy to put this into practise is because developers don't generally create a separate file for EVERY single function, particularly in the case of helpers and especially server-side code. For instance, in PHP would you have a separate file for every possible helper rather than one "helpers.php" file? Or maybe a few files where helpers are placed by category? E.g. array-helpers str-helpers.php etc

I realise tree-shaking isn't applicable to server side code and there is totally different concerns there, but I'm just trying to understand the hesitancy and possible maintainability problems that may come with sticking to the one file per export. I guess it has it's cons and pros.

@garygreen i agree that might explain some of the hesitance; but I’ve been doing this for years and not found any maintainability problems as a result. Other than “wow that seems like a lot of files” i really haven’t heard any explanation of why it’d be a problem.

As for PHP, if they had a proper module system instead of forms of include, i absolutely would - loading 50 functions when i only need to use 1 is silly.

loading 50 functions when i only need to use 1 is silly

Guess your not a fan of frameworks then 😝

I don’t author those :-) i worry about the things i can control, which is my own code.

Not loading unnecessary code also helps with not slowing down your tests.

Prefer default export only occurs when your file exports 1 named item. It goes away if your file exports 2 named items.

lint error:

  export const anything = 'i prefer default export';

lint happiness

  export const anything = 'we coo';
  export const bro = 'only cuz im here';

named export are better for naming consistency

This is unfortunately incorrect, because they can be renamed - the only way to enforce "consistency" is with a linter rule, which would be able to support defaults identically as well as names.

For this argument, and this argument only, the ability to rename is not the key concern or benefit.
It's explicit vs implicit.

Quick! Which one is renamed?

import Thing from "somewhere"
import {TheActualName as Thing} from "somewhere"

@GollyJer only the second one; because the first one never has a name to begin with. You're not renaming anything, you're naming it.

@ljharb
Touche. Good point. I'm wrong.
(not defensive... just true) 😀

only the second one; because the first one never has a name to begin with

Not to be pedantic but technically they both have names. default is still a name, but of course it has special semantics under the hood.

Doing this is technically valid:

import { default as Thing } from 'somewhere';

@garygreen default exports are conceptually not a named export, even if the way the spec is written makes default also be another name. No default export is actually named "default" in a conceptual/human sense.

A default export is what a module is, a named export is what a module has. The module's "name" is its specifier/file path/url.

@ljharb

@garygreen default exports are conceptually not a named export, even if the way the spec is written makes default also be another name.

If you interop with JS, it's clear that @garygreen is right. It is named "default".

@villasv babel's interop isn't the spec, so i'm not sure what you're referring to. Yes, the default export also shows up from the outside as if it were a named export named "default", but again, conceptually, it is not a named export.

default as a named entity is a real thing in the spec in the sense that you can do this when other-script.js has a default export:

import { default as something } from './other-script.js';

This would be more or less the same as just doing:

import something from './other-script.js';

But that's something that's not just "a quirk of babel" and works in browsers that support modules.

Yes, you're totally right - that's how the spec is written.

But conceptually - which has no relationship with what extra things are possible - a default export is not, in fact, just another named export.

A default export is what a module is, a named export is what a module has. Most modules should be something.

It is very simple imho:

  • When you are exporting an "abc", and everywhere it is imported in your codebase, it is imported as an "abc", things easy for everyone.
  • When you are exporting an "abc", but it is imported as "whatever else" in different places in you code, like you can with default exports, you are making things harder for yourself and for other developers.

@ruslanvs named exports can be renamed, as can defaults; the two methods are utterly identical in this regard. If you enforce sameness with a linter, you can do so with either method as well.

@ljharb

  • Can you please provide a sample of how they are identical?
  • "If you enforce sameness with a linter, you can do so with either method as well." - why would you want to do that instead of just eliminating the issue in the first place?

@ruslanvs import { foo as bar } from ‘path’ is legal JS syntax. The “issue” is impossible to eliminate without additional tooling, because JS in no way forces any specific identifier name ever.

@ljharb

  • In your named export sample, you do have the original "foo" that was exported, devs can easily relate the "foo" to "foo". Things are easy, there is no issue. Also, it requires extra work to rename the import.
  • You omitted a default export sample, which in the case you set would be looking like: import bar from 'path'. The original "foo" is totally missing in there, this code is marginally harder to trace and there is no benefit for that, it is a net loss.

@ruslanvs if you follow the other advice in this guide, the default export's name will match the filename anyways, so the information is in the specifier. Nothing is lost.

@ruslanvs I don't find import bar from 'path' harder to trace – isn't it explicitly importing the default export in 'path'?

As far as the prefer-default-export rule is considered, to me it's a reminder to include only closely related stuff in a single module (high cohesion) and not export unnecessarily (encapsulation). There are also valid cases where it makes sense to export multiple symbols from the same module.

@gaurav- can you please share a code of workflow sample, in which it would be easier to trace an export of 'abc' from 'path', while it is being imported as import bar from 'path' as opposed to when it is imported as import { abc } from 'path'?

prefer-default-export is just what it is to me, although you may be associating some good practices with it, it does not stop many other people from misusing default imports.

@ljharb please check out the sample provided by @ljharb above. It shows why " the default export's name will match the filename anyways" does not work.

Question again, what is the benefit of not using language's native way to enforce imports to be more explicit and consistent, using names of the imports with named imports in favor of a less explicit default import, that does does not natively enforce consistent import naming?

That’s not the purpose of named exports - it’s literally zero of the intent.

@ljharb can you please provide any code or workflow sample that would make default exports more useful than named exports? You're yet to show one. By the way, did you notice that some 50% of all posts in this thread are all yours?

Named exports make imports more explicit and consistent than default exports:

  • When you export "abc" and import it by referencing "abc" everywhere in your code, things are easy, straightforward. Named exports are a natural way to do that in JavaScript.
  • When start importing the "abc" as "xyz" or anything else, as is valid in default exports, you make the readability and traceability of the code worse. You gain nothing in return, it is a net loss.

Argument about "exporting just one thing" with default exports does not work: exporting that "just one thing" using named export guarantees that it would also be imported under the same name everywhere, making the code base more traceable and consistent. Default exports lack that.

@ruslanvs Yes, I maintain this guide, so it's appropriate that the majority of comments in the repo are all mine.

There's no code example that matters one way or the other - see https://github.com/airbnb/javascript/issues/1365#issuecomment-548202974:

A default export is what a module is, a named export is what a module has. The module's "name" is its specifier/file path/url.

Keeping the identifier name the same does not aid readability in an objective way.

However, if you disagree with this guide, you are more than welcome to fork it and modify it as you see fit!

@gaurav- can you please share a code of workflow sample, in which it would be easier to trace an export of 'abc' from 'path', while it is being imported as import bar from 'path' as opposed to when it is imported as import { abc } from 'path'?

@ruslanvs I didn't claim one is "easier" than the other. I only wrote that I don't find one to be "harder" than the other; like you seem to find default to be harder than named. In other words, they are the same to me in terms of traceability.

prefer-default-export is just what it is to me, although you may be associating some good practices with it, it does not stop many other people from misusing default imports.

Isn't the whole point of linting rules to help with the best practices that you agree with?
If you don't agree with it, you can define your own linting rules!

I'm curious about the misuse of default though. Could you show some examples or point me to the comments that already have these examples?

@ljharb thank you for your work. If I did not really love arbnb's Javascript ruleset overall, I would not be spending time here today.

The guide is opinionated for a good reason, much of which is about code readability and maintainability - and for 99.99% of rules in this guide - that practical approach makes me love this guide and makes eslint + airbnb style experience overall so phenomenally good.

I do believe that this particular rule is detrimental to code readability and maintainability. I have provided very straightforward reasoning above for that.

However, none of the reasoning provided in this thread, #1365 (comment) including, have addressed the practical reality of code readability and maintainability. Your "There's no code example that matters one way or the other" demonstrates that.

Perhaps, just like any other rule that is revisited from time to time here to make the guide even better, this one should also be revisited with practical code quality and workflow purposes in mind.

Once again, thanks for the overall phenomenally good guide to you and the team.

@gaurav- speaking of how default export does not help embrace "exporting just one thing" - how about this (rather benign) example? -

// util.js
export default {
  doSomething() {},
  doSomethingTotallyUnrelated() {},
  evenMoreUnrelatedData: 'Only I know where to look for this data',
}

// app.js
import appUtil from './util';
appUtil.doSomething();

// MyComponent.jsx
import xyz from './util';
xyz.doSomethingTotallyUnrelated();

Let me know if this is not horrible enough. The point is, enforcing default export for single exports does not address the issue of exporting balls of mud.

With named exports you are at least guaranteed that (a) there is a name to the export, and (b) that this name will show up in every file this thing is imported in.

You continue to assert that these things have value - but without any backing except that it's more readable to you. If that's something you want, fork the guide and change it. It's not something desired in this guide at this time (it's something explicitly unwanted by this guide, in fact)

@ruslanvs that example doesn't fare any better with named exports, at least not to me.

// util.js
export function doSomething() {}

export function doSomethingTotallyUnrelated() {}

export const evenMoreUnrelatedData = 'Only I know where to look for this data';


// app.js
import * as appUtil from './util';
appUtil.doSomething();

// MyComponent.jsx
import * as xyz from './util';
xyz.doSomethingTotallyUnrelated();

This just reiterates my point about them being no different in terms of "traceability".

Modularity is of course not determined merely by the use of default vs named. Like I mentioned earlier, it's poor modularity to keep unrelated code in the same module, even if they are exported as named symbols.

@gaurav-
1) That example was addressing your question on misuse of default import. Hopefully it leaves no doubt that default import itself has no effect on good or bad code modularization practices.
2) Please notice that in your example every instance of the named export is still referred to with its very name in every import - there is no way around it. You can easily grep / global search that name and be guaranteed to find every import instantly. You do not get it with default export (yes there are other ways, but those require marginally more work, so "why?").

Based on maintainer's feedback, this discussion has no effect, hence this is my last comment in this thread unless the context changes. Please feel free to DM.

  1. Going by the language semantics, knowing that default _is_ the module does serve as a guideline/reminder to me, as I mentioned in https://github.com/airbnb/javascript/issues/1365#issuecomment-579432945. Of course, it doesn't automatically make your code good or bad in terms of modularity – something I never suggested.

  2. If it makes sense for your module to export many symbols, or even just one named symbol, please do so by all means if that works better for your project. The prefer-default-export rule doesn't ban named exports. It just checks with you if _maybe_ you wanted that one named export to be _the default_ export. Please read https://github.com/airbnb/javascript/issues/1365#issuecomment-416031279 for my detailed take on it

@ruslanvs named exports can be renamed, as can defaults; the two methods are utterly identical in this regard. If you enforce sameness with a linter, you can do so with either method as well.

The point is, there is no authoritative name for default exports other than "default". Therefore consumers can name the export every possible permutation of characters in a character set. That can make a codebase completely unreadable. In file 1, "foo" refers to "bar" but in file 2 "foo" refers to "baz". If something is exported with a name, you have the option to rename. That is typically used in the case where imports would have naming collisions like:

import { Widget } from "widget-library-1";
import { Widget } from "widget-library-2";

the as semantic is for this specific case where you can

import { Widget } from "widget-library-1";
import { Widget as WidgetAvoidingNameCollision } from "widget-library-2";

This is a common semantic in many languages including .NET languages. Even if you claim that both of these are equivalent, which I would argue they aren't, you've essentially pointed out that ES6 imports are poorly designed with respect to these topics we are discussing and therefore no choice can remedy that other than to redesign it.

  1. If it makes sense for your module to export many symbols, or even just one named symbol, please do so by all means if that works better for your project. The prefer-default-export rule doesn't ban named exports. It just checks with you if _maybe_ you wanted that one named export to be _the default_ export. Please read #1365 (comment) for my detailed take on it

The problem with this argument is let's say I have a file of constants. In version 1 of my application I start out with 1 constant and thus have to use export default. The consumers then consume the default export. Then in version 2 of my application I add another constant, now I have to name it because there cannot be two default exports. Now constant 1 is imported as default (for backwards compatability) and constant 2 is imported named and for no apparent semantic reason. To change constant 1 to a named export means I have to refactor all the existing consumers. Or just use named exports from the beginning because this linter rule is silly. That's why this linter rule makes no sense because you can't apply it to all situations. It's up to the developer to decide which one applies best using best judgment.

@jonyyz for a file of constants, it's appropriate to override this rule, so that even with 1, you'd want it to be a named export. Linter rules don't have to apply to all situations; that's what overrides are for.

@jonyyz for a file of constants, it's appropriate to override this rule, so that even with 1, you'd want it to be a named export. Linter rules don't have to apply to all situations; that's what overrides are for.

And a list of helper functions... and a list of typescript types. You seem to be claiming this situation is normally the rule and there are very few exceptions. I don't think that's true at all. I think it's up to the developer to decide what works best on a case by case basis (the default). There are many cases in software engineering where the answer to "Should I do X?" is "it depends." In other words, there are frequently situations where there are not absolute or objective truths in software engineering. A software engineer who expects that to be the case 100% of the time is a very unhappy software engineer. This is coming from a software engineer who has been around for some time and used to be a more youthful and idealistic engineer. 😄 I have defended many an ivory tower and find it humorous when I look back on my younger self.

Helper functions should be separate files rather than a bag-o-helpers; I'm skeptical that a single list of TS types (as opposed to each type living with each thing it's describing) makes much sense.

It's entirely up to the developer - nobody's forcing you to follow this guide or to use the eslint configuration unmodified. I've explicitly said upthread that there's exceptions, but the rule is highly useful since those exceptions are, in most projects I've worked in, rare.

Wow, this long thread shows that this rule divides opinions.

I use default exports because I'm used to the Java-style of having one file per class, even if I don't use classes much in JavaScript code.

But lately, I'm thinking to switch to the opposite: disallow default exports.

My main reason doesn't have to do with code style, refactoring, or tree-shaking. The problem that I'm having with default exports is with re-exports.

Here is an example of what I mean:
https://codesandbox.io/s/nice-sammet-jem3c?file=/src/index.ts

In the example, there are two components ComponentA and ComponentB, and a parent module called components. The parent module re-exports all the components.

According to the spec: export * from './ComponentA' will not do a default export: https://exploringjs.com/es6/leanpub-endnotes.html#fn-modules_2

But the problem is that sets an export named default!
See the linked example, when you do: import * as components from './components'

The components variable will contain an entry called default that shouldn't be there.
And there is more... the value of the entry depends on the re-export order, meaning that the order of re-exports have side-effects 😱:

export * from './ComponentA'
export * from './ComponentB'

is not the same as

export * from './ComponentB'
export * from './ComponentA'

because it changes the value of components.default.

The example uses Parcel which uses Babel internally. But the problem is also present in plain TypeScript. I couldn't check it with the browser.

Possible solutions:

  1. Stop using export * and do explicit exports. But as you can imagine, as the component library grows it's painful.
  2. Do not use default exports.

I'm leaning towards 2.

Any opinions? How others resolve re-exports with default exports?

You should do export { default as ComponentA } from './ComponentA' when using default exports.

You should do export { default as ComponentA } from './ComponentA' when using default exports.

Yes, I'm doing that, see the example: https://codesandbox.io/s/nice-sammet-jem3c?file=/src/components.ts

The problem is not with export {default as ComponentA} from ... the problem is that I also need to export the non-default stuff: export * from './ComponentA' according to spec this should not export default which is fine (is just two lines of code).

But in practice it does export a value named default which is visible only if you do import * as components (see the example).

@dfernandez-asapp this is a separate topic, but might i suggest that the problem is re-exports :-) your consumers should be importing exactly what they need, from the deep path it's in, as opposed to pulling from a "manifest" export bag that borgs up all the things in your project into one entry point. Treeshaking is only a thing that has an effect when folks are sloppily importing one thing from a big bag-o-things; when using proper deep imports, treeshaking has no value and isn't needed.

@ljharb having a single entry point, doesn't affect treeshaking (if is done right) and is a practice that makes working with ESM modules easier.

If you package ESM+CommonJS, the single entry point hides the implementation detail and the bundler (Webpack, Rollup or Parcel) will pick the ESM declaration and do the treeshaking. Some examples in the wild: Material UI encourages you to use import {} from '@material-ui/.. instead of path imports, API Extractor from MS enforces that you have one entry point only, RxJS does the same, and React does the same (import React from 'react' only works with module interop).

I know it's a different topic, but I disagree with the "export bag that borgs all the things"... in fact having a single entry point is the best way to hide the internal file structure of your package. Treeshaking works fine in that case.

@dfernandez-asapp it doesn't affect it, it causes it to be necessary. You shouldn't need treeshaking at all except to clean up sloppiness.

@ljharb having a single entry point, doesn't affect treeshaking (if is done right) and is a practice that makes working with ESM modules easier.
@dfernandez-asapp it doesn't _affect_ it, it _causes it to be necessary_. You shouldn't need treeshaking at all except to clean up sloppiness.

I suggested a proposal a long time ago that would offer the benefits of tree-shaking as part of the language, but in a predictable way, however it never went anywhere unfortunately.

@Jamesernator there aren't any benefits to it. All it does is attempt - badly and incompletely - to clean up the mess made by importing from modules that export too many things.

I have decided to turn off the default export rule in favour of named exports. This is based on the recommendation of the creator of ESLint Nicolas Zakas. Find more details here: https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/

More importantly, named export add consistency that I find useful in my projects. @ljharb I take your point that one can easily rename the import however the default behaviour of named export vs export default makes named exports more suitable.

The thrust of that article is about not knowing what you're importing, using named exports doesn't actually help this. imo the article does not actually present a good argument for using named exports, just their preference for it.

@ljharb we will have to agree to disagree :smile: . The argument is that using named exports gives you a better experience. That is if you export using export default, when importing the module, you have no indication (at least in VS Code) as to what you are dealing with. If however you export using a named export, VS Code gives you some useful information. Also the re-export statements are less verbose with named exports.

Changing how you write your code because of flaws or gaps in your tooling is a very short-sighted approach (and i'd suggest not re-exporting things anyways; deep import what you need, don't make a big manifest file to import from).

@Fabyao Awesome article link, thanks!

@Fabyao excellent article, @ljharb please reconsider: the upvotes outnumber the downvotes 9.8 to 1 at time of writing. Given most of the feedback is around refactoring and dev speed, this seems to be slowing down a lot of people.

Decisions aren't made by voting, and as I said above, the article does not in fact make any arguments against default exports that hold up to scrutiny.

Not saying that they are, just pointing out that a change would benefit most the participants of this thread. Regarding scrutiny, it's not an objective thing out there in the ether, you're the one scruitananzing, and actively deciding that it does not hold up to your standards, despite an overwhelming majority of participants stating otherwise.

@ljharb Hi Jordan, thanks for your dedication to this discussion! 🙌

I've tried to understand your position on this and stumbled upon the mention of:

The ideal module/file only default-exports one thing, ideally a pure function. There are always exceptions and justifications for deviating from this, of course - but that's the default I start from.

I'd like to hear a little more about the reasoning behind this if possible. What is the thought process exactly behind this definition?

@ljharb I strongly disagree with that statement

Decisions aren't made by voting

Many rules (some more important than named export vs default export) and leaders driving our every day life are decided by voting.

Coming back to our debate about default export, the majority of devs here do not see the benefits of forcing a default export. This is further endorsed by other experts (Nicolas Zakas). To ignore to overwhelming majority against default export is short-sighted (to use your own words).

I am working on a large project with 22 other devs and the benefits of named export are quiet evident. We agreed that the default behaviour of exporting defaults would affect our naming standard. The rule is now off:

rules: { 'import/prefer-default-export': ['off'] }

@Fabyao in this project, decisions aren't made by voting, and they're also not made at all when they contradict internal airbnb consensus. Until that changes, this stands no matter how many people disagree with it.

There's 35 participants in the thread, and less than 300 emoji reactions on the OP. That doesn't represent a majority among Airbnb's frontend engineers, let alone the wider JS community.

@halfzebra that's not a definition, just an opinion formed through years of experience with many large and small codebased.

@ljharb fair enough! I'm coming from a different development environment and probably many of my experiences are not representative.

I respect Airbnb engineers and it would be valuable to hear about the benefits behind practices this rule encourages.
I'm sorry if this looks like an invitation for a debate, I've been furiously advocating against default exports and I'd like to know whether I've made a mistake.

What am I missing?

@halfzebra the points have been enumerated many times in this thread :-) happy to discuss it further if you want to find me in a chat medium!

@ljharb I'm afraid that proposed benefits confuse me more than give an answer 😕

Thanks that would be great! What's the best way to reach out to you?

Best is probably the node slack, or on freenode IRC.

It's amusing to see the same already-addressed points that keep coming back again 🙂

My longer answers are buried in this long thread.

The only new point I'd like to add is the example of a popular library that extensively uses default exports https://github.com/date-fns/date-fns/blob/v2.14.0/src/addDays/index.js#L27

At the end of the day, decide what works best for your project/team and set the rule accordingly. But please also allow for the fact that there are many for whom prefer-default-exports is the preferred default (pun intended), especially the maintainers of this repo.

Interesting points when using with Typescript - just as a thought:

  1. Auto Imports are greatly affected
  2. Typescript has auto fixing when moving files around
  3. VSCode has the Rename Symbol feature

So there can't be justified convictions or categorical opinions? You might disagree with the reasons, but they're stated clearly, and it comes down to a judgement call. Replacing judgement with a vote makes it needlessly manipulable, because you can just canvass for votes. Being able to utter "subjective" doesn't entitle you to being right if you don't explain your reasons. Just a reaction to an "attitude" (someone thinking they're right) is not a reason, and it's also not a contribution to the discussion.

So there can't be justified convictions or categorical opinions? You might disagree with the reasons, but they're stated clearly, and it comes down to a judgement call. Replacing judgement with a vote makes it needlessly manipulable, because you can just canvass for votes. Being able to utter "subjective" doesn't entitle you to being right if you don't explain your reasons. Just a reaction to an "attitude" (someone thinking they're right) is not a reason, and it's also not a contribution to the discussion.

So to this point, I agree actually that having a strong voice and choice makes sense. You are absolutely free to fork the repo and change it -- and if the larger part of the community agrees they would use your fork... and if they don't, you would have your fork for yourself. So, the larger part of the argument is not really relevant.

Changing this would be a massive breaking change at this point anyway. While I am 100% against the rule personally, I am sure it would be insanity should they make the change.

Anyway, not like you can't just overwrite it...


My view on the rule itself is this:

  1. Named exports make it clear throughout the application that you are using a specific value. Using default exports means that each part of the app may have a different name for the same function which makes code harder to reason about.

  2. The argument about changing the name requiring all other parts of the application needing to do the same WAS VALID at one point, but with features like Rename Symbol which most IDE's - and VSCode (which lets be real most of us are using) have -- this became 100% useless.

  3. Named exports make things like Auto Imports that some language features such as TypeScript support. Without named exports, they are far more prone to issue and require context before they are ever capable of suggesting an auto import of a given value.

  4. If you end up needing to export more values from a file as the application grows, it ends up becoming quite messy to either change from default to named as this rule seems to want -- or move to more files being used. With the more files approach - if you want to keep things logically organized that may even mean you have to move things into a folder and make significant changes to the design of the project as it grows and can end up highly disorganized

    • Whereas if you default to named exports then nothing changes, you simply export more values. If you want to use folder approach, you can - just do the folder, create an index file and export named exports from that - and dependent files have no knowledge of the change required.

@bradennapier as has been stated many times, via renaming or re-exports, named exports have precisely the same lack of guarantee for a universal name as default exports do, so this is a non-argument.

A default export is what a module is, a named export is what a module has - you shouldn’t ever need to export another thing that would warrant changing the default, since only one thing can be the module. Make a new module for that.

@slikts I believe they are ways to make your point without insinuating that the person on the other side of the debate is inferior. I find the choice of words by @ljharb quiet aggressive and border line rude. Indeed arrogant. This sort of behaviour kills any chance of a healthy debate and dissuade others to join in. One can have a "categorical opinion" without using words such as:

you screwed up, it's a ridiculous argument, very short-sighted approach

@ljharb - This sort of language is what leads to a toxic environment. On the other hand, some of your answers are very good:

this is a separate topic, but might i suggest that the problem is re-exports :-) ...

I think most readers would appreciate similar wording.

@Fabyao there's no desire for a debate; this is airbnb's style guide, and while I may choose, out of the goodness of my own heart, to entertain discussion, that doesn't mean I'm obligated to spend my free time and my emotional labor fine-tuning every comment.

I hear that you find my wording aggressive and rude; while that wasn't the intention, intentions never matter - only the impact does, and I apologize. I'll try to word things more carefully in the future.

I hadn't realized until now that @citypaul had posted and deleted his comments; I take the deletion to indicate their agreement that they shouldn't have been posted in the first place. @Fabyao similarly, this thread isn't the place for personal attacks; if you have further feedback for me, please give it to me directly.

After reading @bradennapier's points on Typescript, I was curious about whether Typescript has any recommendation on the subject. Did some quick search and found this in their docs: _If you’re only exporting a single class or function, use export default_

This aligns with what I had written some time back in this thread; the thumb rules that work for me: https://github.com/airbnb/javascript/issues/1365#issuecomment-416031279

Just my 2c: After working with both default exports and named ones for a while, I can clearly see that named exports are better DX wise, especially for more junior developers. I get the points on why use the one or the other on concepts etc.... But eventually, as developers, we want our code and IDE to help us write code in an easy way, and up until today, that is only working out with named exports. Default exports give no help by your IDE, and in a big project, will confuse a lot of developers.

Default exports give no help by your IDE, and in a big project, will confuse a lot of developers.

@dietergeerts, if by "DX" and "help by IDE" you mean IntelliSense/AutoComplete type behaviour, then it's available in IntelliJ/Webstorm for default exports and I've also seen it on VSCode of my colleagues.

In large projects, named exports are more likely to cause name collisions and hence more confusion amongst junior and senior developers alike - then you'll have to start defining conventions for aliasing the named exports - which is what you do with default exports anyway! Especially if the codebase is organized as DDD-style bounded contexts:
Bounded Context

Bounded Context

Thx for this example. It's the first actual example in this tread that clarifies why default exports are preferred, and can make sense to use. Though in this scenario, I would still use named exports due to the Developer Experience you get out of it. A lot of major libraries did the move to named exports last year too, because of the Developer Experience. _(I also wonder how much projects really implement in a DDD style, I know it, done it, and in lots of projects that I've worked on, it doesn't add extra value.)_

I, and a lot of other developers I've worked with, never experienced IDE help on default exports, even when trying to get it work and play with the settings, this goes for both WebStorm and VSCode. So if you know how to get the IDE help, could you please share this information, as that would help a lot of people in the future. If this would work, then it's a reason less against default exports.

@dietergeerts
It just works out of the box on IntelliJ
IntelliJ

And while I don't use VSCode, it worked there too (it uses commonjs style by default but you can easily configure it to use the modern syntax)
VSCode
_Code used for the above recordings: https://github.com/gaurav-/default-export-ide-support-demo_

I also wonder how much projects really implement in a DDD style, I know it, done it, and in lots of projects that I've worked on, it doesn't add extra value

I mentioned DDD with Bounded Context (BC) as an example but you don't need to use it verbatim to be prone to name collisions. It's usually the nature of large software projects to have different BCs - whether you practice DDD or not, or identify proper BC boundaries or not (but that's a separate discussion)

Funny that you mentioned libraries moving towards named exports - coincidentally I recently encountered collision of map exported by lodash and rxjs in a code I reviewed. On the other hand, there are examples of libs that extensively use default exports - like date-fns that I had shared earlier (https://github.com/airbnb/javascript/issues/1365#issuecomment-653170555). And then there are frameworks like Angular and Nestjs that tell you not to use barrel files that just import and re-export named exports - although that's for reasons specific to how their dependency injection works and not a comment on which style is better.

The bottom line is that while there may be project-specific (or package/module -specific) reasons for using named exports, the decision of the maintainers to prefer default exports is not an outlandish one!

@gaurav-

I think it will be a setting then, as for our team, it's not working, not in WebStorm.

The bottom line is that while there may be project-specific (or package/module -specific) reasons for using named exports, the decision of the maintainers to prefer default exports is not an outlandish one!

I never said that it was simply a wrong decision. I actually thanked you to show us a practical example of when it is useful, because it helps us to see the situation in which it can be very useful. The fact that there is a huge amount of people not being for the default exports shows that a lot of projects are different and benefit more from using named exports. It's not about what is good and what is bad, as in programming, every option is about compromises. Each option has its pros and cons, and by showing us the example, and actually telling us the main reason for choosing default exports lets us understand the reasoning behind, which in turn helps us decide if we want to use it or not, and being able to understand that it's project specific, and thus can simply be overwritten. Up until now, it was never told why exactly (practically) it was chosen to prefer default exports, nor where examples given, instead, the theory behind modules was being explained, which isn't helping in understanding and not practical. So it would helped to describe this from the start, as is the thing actually asked from the start, to have practical examples and reasons why it is this way.

As for naming collisions, I have yet to experience them, as our files are pretty small, which works great. And in the rare cases we have them, it makes sense to name them specific, which would have been the name that would have been chosen if it was a default export. ("barrel" files should be a big no-no, as it doesn't work well, and is bad for tree-shaking etc.... Imho)

So again, thx you for the example and actual reason that default exports are preferred, it actually answers the question that started this tread: "What is the benefit of prefer-default-export?"

In large projects, named exports are more likely to cause name collisions and hence more confusion amongst junior and senior developers alike

  • default exports cause confusions even on small projects, let alone larger ones, where you'd use DDD.

@ruslanvs that's not my experience maintaining almost over 200 small projects, all of which only use default exports.

Now that @ljharb isn't working for Airbnb, can we reopen and modernize this?

@Exac it's not solely my opinion; i still maintain this project; this is already fully modern and ideal.

Hey @dietergeerts I missed your comment but glad it helped. The part of my comment that you quoted was not directed at you, so apologies if you thought it was for you specifically.

I think it will be a setting then, as for our team, it's not working, not in WebStorm.

Yeah, likely. Works out of the box for me

As for naming collisions, I have yet to experience them, as our files are pretty small, which works great.

In my experience, the likelihood of naming collisions proportional to not much on the file size per se, but simply on the number of exported "names" and the "bounded contexts" (implicit if you don't follow DDD)

The nuance here is the meaning and intended use of modules as per ES2015+. It seems people who prefer named exports over default are doing so for wrong reasons or misconceptions. Nevertheless, like most "best practices", it's not something that would destroy your project if not followed to the tee :)

The rule should be "allow only 1 named export per file".

I guess all these people are too comfortable in their proprietary IDEs to care about how easy it is to refactor code. If you do some regexp search through a codebase that uses only named exports, you will find every file that depends on a named module easily. And if some file renames the named export, the files will still show up in your search and you will immediately see what it renames it to. But with default exports it is so much harder to know which files depend on your exported module, unless you have fancy language server protocol features I guess...... 🙄

Oh yeah, not to mention that when everyone renames default exports to whatever they want, you always have to go look at the imported file to figure out what RenamedDefaultExport really means. Even if you know the module already, you won't recognize it at first sight when it gets renamed.

Good luck when your IDE fails to refactor properly though! 💀💀💀

@Sleepful trivial refactors are not the hardest problem to be solved, nor anywhere close to the most important. If you alter a file, whether it has a default or named exports, the linter and your unit tests will tell you what files you need to fix, which is far more reliable than a regex search. Separately, you can use an internal linter rule to enforce that default export names match the filenames (as airbnb does internally).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ljharb picture ljharb  ·  139Comments

SimenB picture SimenB  ·  34Comments

PandaWhisperer picture PandaWhisperer  ·  44Comments

alexfedoseev picture alexfedoseev  ·  76Comments

okaybenji picture okaybenji  ·  30Comments