airbnb / javascript

JavaScript Style Guide
MIT License
144.65k stars 26.45k forks source link

What is the benefit of prefer-default-export? #1365

Closed CWSpear closed 7 years ago

CWSpear commented 7 years ago

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.

tom10271 commented 6 years ago

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

{
...
    "extends": "airbnb-base",
    "rules": {
        "prefer-default-export": ["warn"],
    },
...
}
lanver34 commented 6 years ago

Quien eres

noreply

SassNinja commented 6 years ago

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 commented 6 years ago

From @basarat: export default can lead to problems

ljharb commented 6 years ago

@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 *.

dandv commented 6 years ago

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

ljharb commented 6 years ago

@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?

basarat commented 6 years ago

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:

basarat commented 6 years ago

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

basarat commented 6 years ago

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:

basarat commented 6 years ago

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:

basarat commented 6 years ago

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:

basarat commented 6 years ago

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:

basarat commented 6 years ago

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:

basarat commented 6 years ago

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:

basarat commented 6 years ago

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

Jamesernator commented 6 years ago

@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.

alex996 commented 6 years ago

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?

Jamesernator commented 6 years ago

@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.

ljharb commented 6 years ago

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.

dietergeerts commented 6 years ago

@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...

ljharb commented 6 years ago

@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.

dietergeerts commented 6 years ago

@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.

gaurav- commented 6 years ago

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.

citypaul commented 5 years ago

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?

Hermholtz commented 5 years ago

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."

ljharb commented 5 years ago

@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.

Hermholtz commented 5 years ago

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.

VRuzhentsov commented 5 years ago

@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

dietergeerts commented 5 years ago

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/

ljharb commented 5 years ago

@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 :-/

dietergeerts commented 5 years ago

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.

ljharb commented 5 years ago

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.

dietergeerts commented 5 years ago

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.

ljharb commented 5 years ago

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.

garygreen commented 5 years ago

@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.

ljharb commented 5 years ago

@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.

garygreen commented 5 years ago

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

Guess your not a fan of frameworks then 😝

ljharb commented 5 years ago

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

gaurav- commented 5 years ago

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

thedanotto commented 5 years ago

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';
GollyJer commented 5 years ago

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"
ljharb commented 5 years ago

@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.

GollyJer commented 5 years ago

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

garygreen commented 5 years ago

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';
ljharb commented 5 years ago

@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.

villasv commented 4 years ago

@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".

ljharb commented 4 years ago

@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.

CWSpear commented 4 years ago

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.

ljharb commented 4 years ago

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.