Why Reading JavaScript Is More Difficult When Default Exports Are Used

Written by baransu | Published 2022/10/16
Tech Story Tags: javascript | programming | software-development | javascript-development | javascript-frameworks | programming-languages | javascript-top-story | best-practices

TLDRUsing default exports hurts your codebase readability and refactorability. The first major problem with default exports is naming. You have to think about name import every time you include a function. It's forcing you to come up with a good name when creating a new function, as it's allowed to export anonymous values. You cannot spot right on what's that function is supposed to do. You can suggest a different name in the code review, but it's adding that complexity which could be easily avoided.via the TL;DR App

In this blog post, I would like to show you how using JavaScript export default hurts your codebase readability and refactorability. I'll also share some tips for using named exports!

I like to treat my code like a lump of clay, it's constantly evolving when as I add new features or change existing ones. Moreso, using default exports not only makes it harder, but it also adds a layer of complexity, especially with already existing code!

Allow me to explain my reasoning!

You can export anonymous functions and values

The first major problem with default exports is naming. You have to think about naming the import every time you include it. Additionally, since it is possible to export anonymous values while creating a new function, you don’t need to think of a catchy name.

Let's take a look at this simple JavaScript file:

// cookies.js
export default () => {
  // baking 🍪 cookies logic
};

// app.js
import makeChocolateChipCookies from "./cookie.js";

You have no idea what your default exported function from cookies.js does. Of course, you can peek into the implementation or count on documentation, but that adds more cognitive load. It’s hard to tell from the jump what the function is supposed to do.

That forces you to name that function in every place you want to use it. And naming things is hard. Maybe you wrote that function yourself, you know exactly what it's doing. But if you have a new team member joining, it's much harder for them to understand what this function is doing.

That can also lead to other team members picking up different names when working on new features or refactoring existing code. And consistency and convention are key for high-performing teams! You can suggest a different name in the code review, but that’s adding that complexity that could be easily avoided in the first place by using named exports.

It's awkward when importing all from a module

JavaScript's import allows you to import everything from the module using import * as X syntax. And default export is just available under default.

// app.js
import * as Cookies from "./cookie.js";

// usage - yikes
Cookies.default();

Of course, if you export one thing it's not that important as you most likely won't use import * as X. But you may want to group multiple things you import by module name for readability.

While I really love grouping module functions, and I think it's really excellent practice for readability, it may not always be optimal for tree shaking and may increase your bundle size!

Default exports and folder-based routing

Frameworks like Next.js or Remix force you to use default exports to define components with folder-based routing. I'm not a big fan of forcing default exports in any case.

TypeScript, at this point, still lacks some kind of support for "template" style exports, where a single file can export a predefined set of optional named exports in addition to the main default export. I would love to see something like that included in the language in the future, based on the popularity of previously mentioned frameworks.

Using named exports

Export each function individually, or export everything at once?

The syntax allows you to use export keyword before each function, type or value.

export function makeCookies() {
  // baking 🍪 cookies logic
}

It's also possible to export multiple things at the same time:

function makeCookies() {
  // baking 🍪 cookies logic
}

function eatCookies() {
  // eating 🍪 cookies logic
}

export { makeCookies, eatCookies };

I tend to prefer first approach as you clearly see whether the function you're reading is exported or not.

Named export alias to the rescue

When you really need to use a different name, named exports allow named export aliases.

// cookies.js
export function makeCookies() {
  // baking 🍪 cookies logic
}

// app.js
import { makeCookies as makeCookiesWithStyle } from "./cookie.js";

Conclusion

Even though we have ES2015 (ES6) for quite some time already, I see a lot of default exports in all types of applications. They were introduced for CommonJS interoperability, and there is no real reason to use them in the internal code. I hope this post will help you convince your team not to use default exports when it's possible.


Also Published here.


If you have some default exports refactoring horror stories, don't hesitate to share them on Twitter!

Resources

List of resources I used when researching this blog post:

Subscribe to my newsletter: https://buttondown.email/cichocinski


Written by baransu | I write about TypeScript, React, and Node.js. Exploring real-time rendering and Elixir in spare time.
Published by HackerNoon on 2022/10/16