Developer Dark Arts: Default Exports
Tags: javascript, architecture, refactorit, webdev
Published: 2020-06-08 12:35:43

So you are starting a ✨ shiny new greenfield 🌱 project. Congrats, not many of us get the opportunity to build something from the ground up.  The architecture decisions you make today will impact everyone that comes after you. Hopefully, after a little convincing, you'll choose to avoid `default` exports.

![With great power comes great responsibility](https://media.giphy.com/media/MCZ39lz83o5lC/giphy.gif)

First some background...

## The JavaScript Module

In modern JS, you have the ability to compartmentalize functionality into separate files commonly referred to as `modules`. Each `module` represents a single unit of work, an entity definition, or a combination of both. Each `module` has its own _lexical scope_ which is academia's fancy term for _variable scoping_... which is my fancy term for the concept that _things_ inside a module are not accessible outside of said module. Things being functions, variables, objects, etc. It also keeps your things from polluting the global namespace.

---

## Exporting Things

> Wait a second. If things are not accessible outside my module how is it that modules are useful?

This is where the `export` keyword comes into play. It defines a contract or sort of micro api to anyone who intends on using your module.

Let's say you have authored the greatest coin flip function ever created. Rather than copy/paste it everywhere you need to flip a coin you decide to extract it into a module appropriately called `coinFlip`. Ya know, to keep your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';
```

In order to expose `coinFlip` to other modules you have an architectural decision to make.



### Option 1: The `default` Export

Those of you coming from [CommonJS modules](https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm) might be familiar with the `default` export pattern. It defines what the _default_ exported functionality is for the module.

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';

export default coinFlip; // <= default export
```

This syntax exposes the `coinFlip` function in a way that allows consumers to `import` it via an unnamed alias.

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';

export default coinFlip;

// decisionMaker.js
import coinFlip from './coinFlip';
```

I say "unnamed" because the name you give imported thing is arbitrary. I could have chosen to import it with any name really. 

For example:

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';

export default coinFlip;

// decisionMaker.js
import aFunctionThatReturnsHeadsOrTails from './coinFlip'; // <= aliased import of a default export
```

The local name of the imported thing is entirely up to the consumer. An important thing to note here is that there can only be one default export per module.

While not immediately apparent, default exports promote large object exports. 

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';
const deprecatedFunction = () => 42;

const randomizer = {
  coinFlip,
  deprecatedFunction,
};

export default randomizer; // <= default exported object
```

Seems legit right? I mean this would future proof your default export. Allowing you to add props to the object as you your module grows but it has one major downside. It isn't tree-shakeable. [Tree shaking](https://en.wikipedia.org/wiki/Tree_shaking) is the process in which consumers of your module transpile and minify their code. Its goal is to remove unused code branches. 

Continuing with the above example, when exporting `randomizer` it cannot be split and dead branches cannot be groomed. Its export is atomic. Consumers get `deprecatedFunction` regardless if they are using it or not. Effectively bloating your code bundles with potentially dead code. This becomes increasingly important in the browser where file size has a major impact on load times and user experience.

_NOTE: Large object exports are a problem regardless of `default` vs named exports. However `default` exports are more prone to this tree shaking pitfall because it's incredibly easy to add a prop to an existing exported object. Often times it's more appealing than refactoring to a named export._


### Option 2: The Named Export

> Ok, so large object exports are bad. What if I have multiple things I need to export from my module?

This is where named exports shine.

Named export syntax is different than default export in that it requires you explicitly name the things you're exporting.

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';

export { coinFlip }; // <= named export
```

This syntax exposes the `coinFlip` function in a way that allows consumers `import` it via a well-defined name.

```javascript
// coinFlip.js
const coinFlip = () => Math.random() < 0.5 ? 'heads' : 'tails';

export { coinFlip };

// decisionMaker.js
import { coinFlip } from './coinFlip';
```

Unlike the default export syntax, you can export as many named exports as you need. 

```javascript
// coinFlip.js
const HEADS = 'heads';
const TAILS = 'tails';
const Result = { HEADS, TAILS };

const coinFlip = () => Math.random() < 0.5 ? Result.HEADS : Result.TAILS;

export { Result, coinFlip };

// decisionMaker.js
import { Result, coinFlip } from './coinFlip';

const result = coinFlip();

if (result === Result.HEADS) {
  console.log('It was heads');
} else {
  console.log('It was tails');
}
```

What if you don't like the exported name or it is named the same as another local variable? Like the default export, when importing you can alias named exports however you want.

```javascript
// decisionMaker.js
import { Result as DiceRollResult, diceRoll } from './diceRoll';
import { Result as CoinFlipResult, coinFlip } from './coinFlip';

// ...
```


### Option 3: Mixed Exports

Default and named exports are not mutually exclusive. You could have a module with a default export and named exports.

```javascript
const HEADS = 'heads';
const TAILS = 'tails';
const Result = { HEADS, TAILS };

const coinFlip = () => Math.random() < 0.5 ? Result.HEADS : Result.TAILS;

export { Result };

export default coinFlip;
```

You'll most often see these 🦄 unicorns while working on brownfield projects that started with default exports and later added functionality to a module. Since default exports don't allow for multiple exports they added named exports to accommodate. That said, I doubt anyone has ever intended to start a project with this export pattern.

---

## Which Option Should I Choose?

Here are a few of the pros & cons that I've seen

### Default Exports

- ✅ Familiar to devs migrating from legacy CommonJS modules
- ❌ Leaves naming up to consumers which doesn't enforce any consistent naming conventions
- ❌ Restricted to a single exported thing per module
- ❌ Promotes large object export anti-pattern
- ❌ Makes tree shaking difficult or impossible in some cases
- ❌ No editor autocomplete/auto-import support

### Named Exports

- ✅ Allows for unlimited exports per module
- ✅ Forces you to name things at time of writing, not consumption
- ✅ Allows for easy tree shaking of unused code
- ✅ Allows for editor autocomplete/auto-import
- ✅ Safe find/replace refactoring
- ❌ Forces consumers to use the exported module name (but allows aliasing)
- ❌ If a named export is poorly named, you may run into a situation where you have to alias the import in _every_ consumer

### Mixed Exports

- ✅ Familiar to devs migrating from legacy CommonJS modules
- ✅ Allows for unlimited exports per module
- ❌ Consumers of the module never know if they want the default export or a named export (adds guesswork)
- ❌ Naming conventions are unclear
- ❌ Safe find/replace refactoring is close to impossible
- ❌ Encourages large object exports which lose the benefit of tree shaking

---

## And the Winner is?

### 🥇 **Named Exports**

Having navigated through and made changes to code bases from all three options, the code bases with named exports are by far the best option. There isn't any mind mapping required to import a given module or its things. There is nothing to decide when creating a new module. Simply export a named thing and go on with your day. Lastly and arguably the most important gain is readability.

What do you think? What am I missing? Where am I wrong? Let me know about your experience in the comments.

---

_Today's post was brought to you by VSCode's "command palette" shortcut:_ <kbd>Command</kbd>+<kbd>Shift</kbd>+<kbd>p</kbd>