Mastering Default and Named Imports in React with lazy

When working with React, especially in larger applications, leveraging lazy loading can significantly improve your app's performance by dynamically loading components only when needed. But did you know that React's lazy behaves differently depending on whether you use default exports or named exports? Understanding these differences is crucial for using React.lazy effectively, ensuring that your components load seamlessly without causing runtime errors.

What Are Default and Named Exports?

Before diving into code examples, let’s clarify these concepts:

  • Default Exports: Each module can have one default export. Default exports are usually used when a module exports a single component, function, or class.

  • Named Exports: A module can have multiple named exports. These are exported by their respective names and require destructuring when imported.

Default Export

A module exporting a single entity is typically defined as:

// Button.jsx
export default Button

This allows us to import it directly:

import Button from './Button'

Named Export

A module with multiple exports is defined as:

// UIComponents.js
export const Button = () => {...};
export const Checkbox = () => {...};

This syntax requires destructuring during import:

import { Button, Checkbox } from './UIComponents'

💡 Best Practice: Use default exports when a module is centered around a single entity (e.g., a single component). Use named exports when multiple related entities are grouped in one module.

How React's lazy Works with Imports

React’s lazy function is a powerful tool for code-splitting and loading components on demand. It only works with default exports, which can sometimes cause confusion when working with named exports.

Default Export with lazy

Using React.lazy is straightforward when the component is a default export:

const Button = React.lazy(() => import('./Button'))

Named Export with lazy

Things get a bit more complex when using lazy with named exports. Consider the following named export:

export const Modal = () => {...};

If you try to import it directly using lazy, it won’t work:

// This will throw an error!
const Modal = React.lazy(() => import('./UIComponents'))

To resolve this, we need to map the named export to a default key:

const Modal = React.lazy(() =>
  import('./UIComponents').then((module) => ({
    default: module.Modal,
  })),
)

Why Does React.lazy Require Default Exports?

The reason lies in how dynamic imports work. When using React.lazy, the function expects a default export to be returned because it can’t directly handle named exports. By wrapping the named export inside a default key, we ensure that lazy receives a structure it can work with.

Refactoring Pattern: Switching from Named to Default Exports

In some cases, refactoring a module to use default exports instead of named exports can simplify the codebase. Consider a component library with many named exports:

// UIComponents.js
export const Modal = () => {...};
export const Tooltip = () => {...};

If the module primarily focuses on a single component (e.g., Modal), you might refactor it like this:

const Modal = () => {...};
export default Modal;

This makes the import much cleaner:

import Modal from './UIComponents'

🔄 Refactoring Tip: Switch to default exports when the primary entity of a module is used more frequently than others.

Practical Use Case: Combining lazy with Named Exports

Sometimes, it’s not feasible to switch to default exports, especially when maintaining shared modules. In such scenarios, using the then pattern is a good compromise:

const Modal = React.lazy(() =>
  import('./UIComponents').then((module) => ({
    default: module.Modal,
  })),
)

When to Use Each Approach?

  • Use default exports when building standalone components.
  • Use named exports for utility functions, shared constants, or modules with multiple components.
  • Use the then pattern with React.lazy when named exports are necessary.

Wrapping Up

Understanding the nuances between default and named exports can save you from common pitfalls, such as unexpected import errors or cumbersome refactoring. By applying these concepts, you can ensure that your React codebase is both modular and maintainable.

📌 Key Takeaways:

  1. Use default exports for single-component modules.
  2. Use named exports for grouped or utility modules.
  3. React’s lazy requires a default export, so use the then pattern for named exports.

Mastering these distinctions will help you organize your React components more effectively and leverage dynamic imports to enhance performance.