JavaScript modules rely on import and export to organize code, share logic, and reduce complexity across large applications. These keywords enable explicit contracts between files, improving maintainability and tooling support.
By defining clear entry and exit points for functionality, import and export shape how modern bundlers, loaders, and runtimes handle dependencies in both browser and Node environments.
| Concept | Browser Support | Node Support | Typical Tooling |
|---|---|---|---|
| ES Module syntax | Modern evergreen browsers | ES Modules via .mjs or "type": "module" | Webpack, Vite, Rollup |
| Export types | Named and default widely supported | Same rules as browser modules | Babel, TypeScript transforms |
| Interoperability | Can bridge CommonJS via tools | Mixed CommonJS and ESM with caveats | Dynamic import(), module resolution plugins |
| Static analysis | Tree-shaking supported in modern bundlers | Respect sideEffects flag in package.json | Scope hoisting, treeshake optimizations |
Named Exports in Modular JavaScript
Named exports allow multiple values to leave a single module, which makes explicit imports clearer and more self-documenting. Each binding is independent, so consumers can destructure only what they need.
Declaration and Import Patterns
You can declare several named exports in a source file and import them with matching names wrapped in curly braces. This pattern supports forward refactoring, because tools can trace usages across the codebase and safely rename identifiers.
When defaults are unnecessary, named exports keep the interface lightweight, encourage explicit dependency graphs, and reduce merge conflicts in collaborative projects with many contributors.
Default Exports and Use Cases
A default export provides a single primary value from a module, which is handy for functions, classes, or configuration objects that represent the core abstraction of a file. Many libraries use a default export for the main component or utility.
Mixing with Named Bindings
You can combine default and named exports in one module, but the syntax on the importing side must differentiate them. This flexibility helps incrementally migrate legacy code while maintaining clear separation between primary and secondary exports.
Default exports simplify imports when there is an obvious main entity, yet overuse can obscure intent, so evaluate whether a named export might better communicate the module contract.
Re-exports and Composition Patterns
Re-exporting allows a module to re-export bindings from another module, creating layered abstractions and façades that simplify public APIs. This technique is common in design systems, shared toolkits, and organized directory structures.
Transparent Re-export Techniques
Using export { name } from 'module' preserves the original binding name while forwarding it, keeping refactorings predictable. Re-export * as namespace is rare and generally discouraged in strict mode due to unclear static analysis.
Strategic re-export points centralize imports, reduce duplication across entry files, and make it easier to enforce architectural boundaries in large applications.
Dynamic Import and Runtime Loading
Dynamic import() returns a promise that resolves to the module namespace, enabling code-splitting, conditional loading, and on-demand features without blocking initial rendering. This pattern is essential for performance in complex user interfaces.
Handling Asynchronous Modules
Because dynamic import is asynchronous, you should plan error handling with catch or try/catch in async contexts. Loaders and bundlers can generate separate chunks, so understanding your toolchain’s output helps optimize caching and preloading strategies.
Server-side environments also support dynamic import, which lets you isolate experimental features, run tests in separate contexts, or load plugins safely without affecting the main runtime.
Modern JavaScript Architecture and Best Practices
Effective use of import and export shapes a maintainable architecture by defining clear module boundaries, reducing hidden dependencies, and enabling scalable refactoring across teams.
- Prefer named exports when exposing multiple related values to improve readability and refactoring safety.
- Use default exports sparingly for primary abstractions, and document the rationale for each default choice.
- Leverage re-exports to build coherent public APIs and reduce duplication in entry points.
- Apply dynamic import() for lazy loading routes, components, or heavy utilities to enhance performance.
- Configure tooling and package.json fields to enforce module styles and catch interop issues early.
FAQ
Reader questions
How do named and default imports interact when migrating from CommonJS?
When migrating from CommonJS, default imports often correspond to the module.exports object, while named imports map to individual properties. Use tools like Babel or TypeScript with appropriate presets to automate the transition and verify side effects.
Can I mix export * and named re-exports without naming conflicts?
Mixing export * and explicit named re-exports can cause naming clashes, so prefer explicit exports when you need deterministic outputs. Tools and linters can detect conflicts and guide you toward safer composition patterns.
What role does "type": "module" play in Node when using import export?
The "type": "module" field in package.json tells Node to treat .js files as ES modules, enabling top-level await and native import export. Without this setting or the .mjs extension, Node will treat files as CommonJS by default.
How do tree shaking and sideEffects config affect import export behavior?
Setting sideEffects false or listing risky files helps bundlers drop unused exports during production builds. Accurate configuration ensures tree shaking works reliably, reducing bundle size without removing necessary polyfills or global patches.