Is using default exports considered a bad practice? Here’s why!
When writing React apps or modern JavaScript in general, we often create files with a set of reusable functions or classes. People tend to use default exports in such cases and I'm going to explain, why it's not the best idea to use them.
What is a default export?
The origin of default exports comes from a willingness to replicate the functionality of exporting modules from CommonJS spec to ES6. So what is a default export basically? Let's have a look at the following piece of code:
// src/components/SomeComponent.js
export default class SomeComponent {
...
}
So you can import it into another file like in the following example:
// src/App.js
import SomeComponent from './components/SomeComponent';
After importing it you can use it however you like in the code of App.js
file. The important thing to note is that you don't have to use SomeComponent
as a name of imported code. It can be literally anything, because there's no strict convention here.
What is a named export?
Comparing to the default exports example it looks differently. First of all there's no default
keyword.
// src/components/SomeComponent.js
class SomeComponent {
...
}
export const OtherComponent = () => {
...
}
export { SomeComponent };
So you can import it into another file like in the following example:
// src/App.js
import { SomeComponent, OtherComponent } from './components/SomeComponent';
So what are the main differences between both approaches and why you might consider using default exports as a bad practice? The explanation is in the next section.
The fight
Let's compare both approaches. I'll try to explain you which of these approaches is better and why.
Naming convention strictness
Like I mentioned earlier, in the default export section, using default exports and importing into another file, you are not restricted to use a component name (or class name, or whatever) defined in the definition file. While importing you can change its name into anything that you find reasonable.
// src/components/SomeComponent.js
export default class SomeComponent { ... }
// src/App.js
import AnyComponent from './components/SomeComponent'
In case of named exports it's not so easy to use any name for imported piece of code. You are restricted to use names defined in the definition file of a piece code.
// src/components/SomeComponent.js
class SomeComponent { ... }
export { SomeComponent };
// src/App.js
import { SomeComponent } from './components/SomeComponent';
But what if you want to change the name of imported component? It's not complicated:
// src/components/SomeComponent.js
class SomeComponent { ... }
export SomeComponent;
// src/App.js
import { SomeComponent as AnyComponent } from './components/SomeComponent';
It might be useful when importing components with similar names from different sources. In the case when you're exporting many components with similar names from many different files, it might be useful to set some sort of namespace for them. Let's take a look:
// src/components/SomeComponent.js
class Button { ... }
export const Panel = () => {};
export const config = {};
export Button;
// src/App.js
import * as UI from './components/SomeComponent';
function App () {
doSomething(UI.config);
return (
<UI.Panel>
<h1>Title</h1>
<UI.Button />
</UI.Panel>
)
}
The last way of importing components is commonly used in React environment for importing React library in TypeScript projects:
import * as React from 'react'
In my opinion having multiple ways of importing components versus one unreliable way of importing them is much better in terms of bulding quality apps. You can accomodate imports to your needs.
Error reporting
Spotting errors early is extremely important in software development. From my experience using default exports is error-prone solution because you don't know whether a specific piece of code exists in a file. When I'm using named exports my code editor can early spot errors by checking whether an imported component exists in a source file. I don't have to worry about mistyping component names. And there we are, typos, these are the most problematic errors, because sometimes they tend to be different to spot a first glance, especially when doing code reviews in apps like Github or Gitlab.
Long term code mainteinance
This one is pretty important when building professional apps, not just pet projects that will never get into the public. Choosing the right exporting convention, I mean using named exports over default exports increases code maintainability a lot. Let's imagine a situation when you're doing a code refactor. You're looking for occurences of a specific component in a codebase. With named exports it's pretty simple, because the imported component name usually is the same as exported one. With default exports it's not that easy, the component names might differ, sometimes a lot. It can have a different name, the name can have a typo. It's all confusing and makes code mainteinance harder.
Default exports as necessary evil sometimes
Sometimes you might be forced to use default exports anyway. Because of other tools that are used when developing React apps. Such tool's developers didn't accomodate it for using code exported using named exports convention. For instance when lazy loading React components. It's a code design decision made by React team. At the end you have to write something like the following, in order to lazy load your React component:
// e.g. App.js
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App {
return (
<div>
<Suspense fallback={<div>Loading ...</div>}>
<LazyComponent />
</Suspense>
</div>
)
}
Summary
In projects I'm working on I'm using named exports because it makes a codebase more predictable and less error-prone. I prefer getting errors as soon as possible when exporting non-existing components and I love having classes names and component names the same as in the their source files. It helps me a lot in reading the code.