Closed CWSpear closed 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.
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
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';
@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.
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. 😄
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 sourceconst
/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.