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.

ljharb commented 7 years ago

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.

deckar01 commented 7 years ago

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

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

coryhouse commented 6 years ago

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

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

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

ljharb commented 6 years ago

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

https://blog.neufund.org/why-we-have-banned-default-exports-and-you-should-do-the-same-d51fdc2cf2ad?source=linkShare-fdf9efd749e0-1511609608

ljharb commented 6 years ago

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

KayakinKoder commented 6 years ago

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

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

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

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

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

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

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

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

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

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

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

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

ljharb commented 6 years ago

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

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

Although this rule encourages single default exports, it does not discourage multiple named exports.

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

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

aboyton commented 6 years ago

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.

ljharb commented 6 years ago

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

adamchenwei commented 6 years ago

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.

ljharb commented 6 years ago

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

tom10271 commented 6 years ago

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

ljharb commented 6 years ago

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.

deckar01 commented 6 years ago

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.

Jamesernator commented 6 years ago

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

dietergeerts commented 6 years ago

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.

Jamesernator commented 6 years ago

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

dietergeerts commented 6 years ago

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

deckar01 commented 6 years ago

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

dietergeerts commented 6 years ago

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

ljharb commented 6 years ago

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

gregplaysguitar commented 6 years ago

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.

ljharb commented 6 years ago

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

gaurav- commented 6 years ago

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

gregplaysguitar commented 6 years ago

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)

ljharb commented 6 years ago

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

gregplaysguitar commented 6 years ago

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.

Jamesernator commented 6 years ago

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

gregplaysguitar commented 6 years ago

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.

ljharb commented 6 years ago

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

CWSpear commented 6 years ago

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