Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
CWSpear opened this issue Apr 10, 2017 · 90 comments

Comments

Projects
None yet
@CWSpear
Copy link

commented Apr 10, 2017

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.

@ljharb

This comment has been minimized.

Copy link
Member

commented Apr 10, 2017

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.

@ljharb ljharb added the question label Apr 10, 2017

@deckar01

This comment has been minimized.

Copy link
Contributor

commented Aug 7, 2017

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.

@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 7, 2017

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

@coryhouse

This comment has been minimized.

Copy link

commented Nov 25, 2017

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.

@deckar01

This comment has been minimized.

Copy link
Contributor

commented Nov 26, 2017

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

@ljharb

This comment has been minimized.

Copy link
Member

commented Nov 26, 2017

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

@aboyton

This comment has been minimized.

Copy link

commented Nov 26, 2017

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.

@ljharb

This comment has been minimized.

Copy link
Member

commented Nov 27, 2017

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

@ljharb

This comment has been minimized.

Copy link
Member

commented Nov 28, 2017

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

@KayakinKoder

This comment has been minimized.

Copy link

commented Dec 8, 2017

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

@deckar01

This comment has been minimized.

Copy link
Contributor

commented Dec 8, 2017

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.

@ljharb

This comment has been minimized.

Copy link
Member

commented Dec 8, 2017

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

@ha404

This comment has been minimized.

Copy link

commented Dec 13, 2017

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

This comment has been minimized.

Copy link
Member

commented Dec 13, 2017

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

@ha404

This comment has been minimized.

Copy link

commented Dec 13, 2017

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

If I were to import a default, I could do whatever I please without the keyword as to rename my import (all of these can be importing the same function):

// somewhere.js
export default function specificNamedFunction () { ... }
import blah from 'somewhere';
import whoop from 'somewhere';
import anythingInTheWorld from 'somewhere';
@ljharb

This comment has been minimized.

Copy link
Member

commented Dec 13, 2017

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

@ha404

This comment has been minimized.

Copy link

commented Dec 14, 2017

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 💀

@ljharb

This comment has been minimized.

Copy link
Member

commented Dec 14, 2017

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.

@ha404

This comment has been minimized.

Copy link

commented Dec 14, 2017

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?

@ljharb

This comment has been minimized.

Copy link
Member

commented Dec 14, 2017

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.

@ha404

This comment has been minimized.

Copy link

commented Dec 14, 2017

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

@ljharb

This comment has been minimized.

Copy link
Member

commented Dec 14, 2017

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

@FireyFly

This comment was marked as resolved.

Copy link

commented Dec 14, 2017

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)

@deckar01

This comment has been minimized.

Copy link
Contributor

commented Dec 14, 2017

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.

@ljharb

This comment was marked as resolved.

Copy link
Member

commented Dec 14, 2017

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

@aboyton

This comment has been minimized.

Copy link

commented Dec 14, 2017

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.

@Jamesernator

This comment has been minimized.

Copy link

commented Aug 23, 2018

@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

This comment has been minimized.

Copy link

commented Aug 23, 2018

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

This comment has been minimized.

Copy link

commented Aug 23, 2018

@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

This comment has been minimized.

Copy link
Member

commented Aug 23, 2018

Regarding #1365 (comment): conceptually, default exports are what a module is, named exports are what a module has. This is a core language concept.

Re #1365 (comment): have you used importjs? It in fact works with both, just as you'd expect.

Re #1365 (comment): 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 #1365 (comment), 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 #1365 (comment), 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

This comment has been minimized.

Copy link

commented Aug 23, 2018

@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

This comment has been minimized.

Copy link
Member

commented Aug 23, 2018

@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

This comment has been minimized.

Copy link

commented Aug 23, 2018

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

This comment has been minimized.

Copy link

commented Aug 26, 2018

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

This comment has been minimized.

Copy link

commented Oct 29, 2018

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?

@chojrak11

This comment was marked as disruptive content.

Copy link

commented Oct 31, 2018

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

This comment was marked as off-topic.

Copy link
Member

commented Oct 31, 2018

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

@chojrak11

This comment was marked as off-topic.

Copy link

commented Oct 31, 2018

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

This comment has been minimized.

Copy link

commented Nov 13, 2018

@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

This comment has been minimized.

Copy link

commented Jan 19, 2019

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

This comment has been minimized.

Copy link
Member

commented Jan 19, 2019

@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

This comment has been minimized.

Copy link

commented Jan 19, 2019

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

This comment has been minimized.

Copy link
Member

commented Jan 19, 2019

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

This comment has been minimized.

Copy link

commented Jan 19, 2019

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

This comment has been minimized.

Copy link
Member

commented Jan 19, 2019

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

This comment has been minimized.

Copy link

commented Jan 21, 2019

@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

This comment has been minimized.

Copy link
Member

commented Jan 21, 2019

@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

This comment has been minimized.

Copy link

commented Jan 21, 2019

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

Guess your not a fan of frameworks then 😝

@ljharb

This comment has been minimized.

Copy link
Member

commented Jan 21, 2019

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

@gaurav-

This comment has been minimized.

Copy link

commented Jan 22, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.