React 8 ⚛️ Exporting and Importing Components
A React application is a collection of components that reference each other. One component renders another, which renders a third, and the structure of the application is the structure of these references. The mechanism that connects them is the JavaScript module system: a component is exported from its file and imported into the files that use it. Without exports and imports, every component would have to live in a single file, and the file would be unmanageable.
The module system in React is not React-specific; it is standard JavaScript. The two dominant formats are ES modules (import/export) and CommonJS (require/module.exports). Modern React projects use ES modules exclusively, and the tooling — Vite, Next.js, Create React App — expects them. The syntax is simple, but the choices matter: default exports versus named exports, when to use each, how to re-export from index files, and how to avoid the circular dependencies that break imports.
This chapter covers the syntax of export and import, the difference between default and named exports, the patterns for organizing component files, re-exporting from barrel files, dynamic imports for code splitting, and the common errors that arise from mismatched exports and imports.
Key point: Each component lives in its own file and is exported. Other files import it. Use default exports for one-component-per-file, named exports for multiple exports per file, and index files for convenient grouped imports. Match the import form to the export form exactly.
Why module exports and imports exist
The single-file problem. An application with hundreds of components cannot live in one file. Without modules, every component would be global, every name collision would be a bug, and no component could be tested or reused in isolation. Modules give each file its own scope, and exports define what is visible outside that scope.
The dependency graph problem. Components depend on other components. A UserProfile renders an Avatar and a UserInfo, which renders a Badge. The import statements make these dependencies explicit, and the bundler builds a dependency graph from them. This graph determines what is included in the bundle and in what order modules are initialized.
The tree-shaking problem. Bundlers remove unused code. For tree-shaking to work, the bundler needs to know which exports are used and which are not. Named exports are statically analyzable; a bundler can see that import { Button } from "./ui" uses only Button and drop the other exports. Default exports are also analyzable, but the flexibility of renaming them on import makes some optimizations harder.
The testing problem. Tests import the component they exercise. If the component is exported, the test can import it directly, render it with known props, and assert on the output. If the component is not exported, it cannot be tested in isolation. Exporting is what makes a component a unit.
The circular dependency problem. Component A imports component B, and component B imports component A. This is legal in JavaScript but produces undefined at the moment one of them is evaluated. Understanding how the module system resolves circular imports, and how to avoid them, prevents a class of bugs that are confusing to debug.
a. Default exports
A default export exports a single value from a module. In React, it is commonly used when a file contains one component.
// Button.jsx
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
export default Button;
The import uses any name the importer chooses, without braces:
// App.jsx
import Button from "./Button";
function App() {
return <Button label="Click" onClick={handleClick} />;
}
Because the importer chooses the name, the export and the import can use different names:
import MyButton from "./Button"; // legal, confusing
The convention is to use the same name as the component, which keeps the code searchable. The default export is also commonly combined with the function declaration:
// Button.jsx
export default function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
This form exports and declares in one statement. It is the most concise pattern for a single-component file.
b. Named exports
A named export exports a value under a specific name. A module can have many named exports.
// ui.jsx
export function Button({ label }) {
return <button>{label}</button>;
}
export function Input({ value, onChange }) {
return <input value={value} onChange={onChange} />;
}
export const VARIANT = "primary";
The import uses braces and the exact exported names:
// App.jsx
import { Button, Input, VARIANT } from "./ui";
Names can be aliased with as:
import { Button as PrimaryButton } from "./ui";
Named exports are statically analyzable, which supports tree-shaking and makes it obvious to tooling which exports a module uses. For a file that exports multiple components or utilities, named exports are the right choice.
A file can have both default and named exports, though this is less common in React code:
export default function Button({ label }) {
return <button>{label}</button>;
}
export const ButtonVariant = "primary";
import Button, { ButtonVariant } from "./Button";
c. Choosing between default and named exports
The choice is not arbitrary. Each has a place, and consistency within a project matters more than the specific rule.
Default exports fit one-component-per-file. When a file’s purpose is to define a single component and export it, a default export is natural. import Button from "./Button" reads cleanly, and the file name matches the component name.
Named exports fit multiple exports per file. When a file exports several components or a component and its helpers, named exports are appropriate. import { Button, Input } from "./ui" makes the imports explicit.
Named exports improve refactoring. Renaming a component with a named export is a single rename; the import sites all use the same name. With a default export, the importer may use a different name, and a rename in the exporting file does not propagate.
Named exports improve searchability. Searching for Button finds both the definition and the imports if the name matches. With default exports, a search for the component name finds the definition and the conventional imports, but an importer that aliased the name is invisible to that search.
Default exports are the historical React convention. Early React projects used default exports almost exclusively, and many codebases and tutorials follow that pattern. Named exports have become more common as the ecosystem has matured, but both are widespread.
The practical recommendation: pick one convention for a project and apply it consistently. Many teams use default exports for page-level components and named exports for reusable UI components, or default exports everywhere, or named exports everywhere.
d. File organization and index files
A common pattern is to organize components into folders and provide an index file that re-exports them.
components/
Button/
Button.jsx
Button.css
Input/
Input.jsx
Input.css
index.js
The index.js re-exports the components:
// components/index.js
export { default as Button } from "./Button/Button";
export { default as Input } from "./Input/Input";
Consumers import from the folder:
import { Button, Input } from "./components";
This pattern, called a barrel file, provides a single entry point for a group of related modules. It simplifies imports and creates a public API for the folder.
Barrel files have trade-offs. They can slow down build times because the bundler must resolve the entire barrel even when only one export is used. They can also cause circular dependency issues when components inside the barrel import from the barrel. In large projects, some teams avoid barrel files and import directly from the component file.
// Direct import without barrel
import Button from "./components/Button/Button";
The direct import is more explicit and avoids the barrel’s overhead, at the cost of longer import paths.
e. Importing CSS and assets
React components often import CSS files and assets. The bundler handles these imports and includes the CSS or asset in the output.
import "./Button.css";
function Button({ label }) {
return <button className="btn">{label}</button>;
}
CSS imports are side-effect imports: they have no bindings, and their purpose is to load the stylesheet. Asset imports return a URL:
import logo from "./logo.svg";
function Header() {
return <img src={logo} alt="Logo" />;
}
The bundler transforms logo.svg into a URL that points to the emitted asset. In Vite, this works for most asset types; in webpack, it requires the appropriate loader.
CSS modules are a variant where class names are scoped to the component:
import styles from "./Button.module.css";
function Button({ label }) {
return <button className={styles.primary}>{label}</button>;
}
The bundler generates unique class names and returns an object mapping the original names to the generated ones.
f. Dynamic imports and code splitting
A static import is evaluated when the module is loaded. A dynamic import() returns a promise that resolves to the module, and it can be used to load code on demand.
function App() {
const [Modal, setModal] = useState(null);
const loadModal = async () => {
const module = await import("./Modal");
setModal(() => module.default);
};
return (
<div>
<button onClick={loadModal}>Open Modal</button>
{Modal && <Modal />}
</div>
);
}
The bundler splits the dynamically imported module into a separate chunk that is loaded only when the import executes. This reduces the initial bundle size.
React’s lazy and Suspense build on dynamic imports to provide a declarative API for code splitting:
import { lazy, Suspense } from "react";
const Modal = lazy(() => import("./Modal"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Modal />
</Suspense>
);
}
The lazy function wraps a dynamic import and returns a component that loads the module on first render. Suspense provides a fallback while the module is loading. This pattern is common for route-level components in large applications.
g. Common import errors
Mismatched export and import form. Importing a default export with braces, or a named export without braces, produces undefined.
// Button.jsx exports default
export default function Button() {}
// Wrong: Button is undefined
import { Button } from "./Button";
// Correct
import Button from "./Button";
Case sensitivity. File systems on Linux and macOS are case-sensitive. import Button from "./button" fails if the file is Button.jsx.
Missing file extension. In some configurations, the extension is optional; in others, it is required. Node.js ESM requires it; bundlers often do not.
import Button from "./Button"; // works in bundlers
import Button from "./Button.jsx"; // always works
Circular imports. When two modules import each other, one of them sees the other as undefined at evaluation time. The fix is to restructure so the dependency is one-directional, or to move the shared code to a third module.
Barrel file circularity. A component inside a barrel folder imports from the barrel, creating a cycle. The fix is to import directly from the component file rather than the barrel.
Complete Example Session
// ============================================
// PART 1: DEFAULT EXPORT
// ============================================
// Button.jsx
export default function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// ============================================
// PART 2: DEFAULT IMPORT
// ============================================
// App.jsx
import Button from "./Button";
function App() {
return <Button label="Click me" onClick={() => alert("clicked")} />;
}
// ============================================
// PART 3: NAMED EXPORTS
// ============================================
// ui.jsx
export function Card({ children }) {
return <div className="card">{children}</div>;
}
export function Badge({ count }) {
return <span className="badge">{count}</span>;
}
// ============================================
// PART 4: NAMED IMPORTS
// ============================================
// App.jsx
import { Card, Badge } from "./ui";
function App() {
return (
<Card>
<Badge count={5} />
</Card>
);
}
// ============================================
// PART 5: ALIASING IMPORTS
// ============================================
import { Button as PrimaryButton } from "./ui";
import { Button as SecondaryButton } from "./ui-secondary";
// ============================================
// PART 6: BARREL FILE
// ============================================
// components/index.js
export { default as Button } from "./Button/Button";
export { default as Input } from "./Input/Input";
export { Card, Badge } from "./ui";
// ============================================
// PART 7: IMPORTING FROM BARREL
// ============================================
import { Button, Input, Card } from "./components";
// ============================================
// PART 8: IMPORTING CSS
// ============================================
import "./Button.css";
export default function Button({ label }) {
return <button className="btn">{label}</button>;
}
// ============================================
// PART 9: DYNAMIC IMPORT
// ============================================
import { lazy, Suspense } from "react";
const Modal = lazy(() => import("./Modal"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Modal />
</Suspense>
);
}
// ============================================
// PART 10: COMMON ERROR — MISMATCHED FORM
// ============================================
// Button.jsx exports default
export default function Button() { return <button />; }
// Wrong: Button is undefined
import { Button } from "./Button";
// Correct
import Button from "./Button";
These ten parts cover default exports, named exports, aliasing, barrel files, CSS imports, dynamic imports, and the most common import error. Each pattern is idiomatic modern React with ES modules.
Quick Reference
Export Syntax
| Form | Syntax | Import |
|---|---|---|
| Default | export default function B() {} | import B from "./B" |
| Named function | export function B() {} | import { B } from "./B" |
| Named const | export const B = () => {} | import { B } from "./B" |
| Re-export default | export { default as B } from "./B" | import { B } from "./index" |
| Re-export named | export { B } from "./B" | import { B } from "./index" |
| Export all | export * from "./B" | import { B } from "./index" |
Import Syntax
| Form | Syntax |
|---|---|
| Default | import B from "./B" |
| Named | import { B } from "./B" |
| Aliased named | import { B as C } from "./B" |
| Namespace | import * as UI from "./ui" |
| Side effect | import "./styles.css" |
| Dynamic | const m = await import("./B") |
Default vs Named
| Aspect | Default | Named |
|---|---|---|
| Exports per file | One | Many |
| Import name | Importer chooses | Must match |
| Braces | No | Yes |
| Tree-shaking | Yes | Yes |
| Refactoring | Rename in exporter only | Rename propagates |
| Convention | One component per file | Multiple exports per file |
Common Errors
| Error | Cause | Fix |
|---|---|---|
undefined import | Wrong form (braces vs no braces) | Match import to export form |
| Module not found | Case mismatch or missing extension | Check exact file name |
| Circular dependency | A imports B, B imports A | Restructure or extract shared code |
| Barrel cycle | Component imports from barrel | Import directly from component |
Best Practices
✅ Do This:
// One component per file, default export
export default function Button({ label }) { return <button>{label}</button>; }
// Import with matching name
import Button from "./Button";
// Multiple exports, named
export function Card({ children }) { return <div>{children}</div>; }
import { Card } from "./ui";
// Barrel for grouped imports
export { default as Button } from "./Button/Button";
import { Button, Card } from "./components";
❌ Don’t Do This:
// Mismatched form
import { Button } from "./Button"; // ❌ if Button is default export
import Button from "./ui"; // ❌ if ui exports named Card only
// Circular import
// A.jsx imports B, B.jsx imports A // ❌ undefined at evaluation
// Direct import from barrel in barrel folder
import { Button } from "../index"; // ❌ circular in folder
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Import is undefined | Wrong export/import form | Use default import for default exports |
| Module not found | Case mismatch or missing extension | Match the exact file path |
| Circular dependency error | Two modules import each other | Restructure to one-way dependency |
| Barrel file slow build | Bundler resolves entire barrel | Import directly from component file |
| CSS not applied | CSS import missing | Add import "./Component.css" |
| Dynamic import fails | Wrong path in import() | Verify the path resolves at build time |
| Tree-shaking ineffective | Default exports with side effects | Use named exports; avoid side effects |
Real-World Examples
1. Default Export
// UserCard.jsx
export default function UserCard({ user }) {
return <div>{user.name}</div>;
}
2. Default Import
import UserCard from "./UserCard";
3. Named Exports
// ui.jsx
export function Button({ label }) { return <button>{label}</button>; }
export function Input({ value }) { return <input value={value} />; }
4. Named Imports
import { Button, Input } from "./ui";
5. Aliased Import
import { Button as SubmitButton } from "./ui";
6. Barrel File
// components/index.js
export { default as Button } from "./Button/Button";
export { default as Input } from "./Input/Input";
7. Import from Barrel
import { Button, Input } from "./components";
8. CSS Import
import "./Button.css";
9. Dynamic Import with lazy
const Modal = lazy(() => import("./Modal"));
10. Re-export
export { default as Button } from "./Button";
Visual
Export and Import Forms
┌──────────────────────────────────────────────────────────────┐
│ MATCH THE EXPORT FORM TO THE IMPORT FORM │
│ │
│ DEFAULT EXPORT: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ export default function Button() {} │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ import Button from "./Button"; │ │
│ │ (no braces, name is chosen by importer) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ NAMED EXPORT: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ export function Button() {} │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ import { Button } from "./Button"; │ │
│ │ (braces, name must match exactly) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Mixing the forms produces undefined. │
└──────────────────────────────────────────────────────────────┘
Component File Structure
┌──────────────────────────────────────────────────────────────┐
│ TYPICAL REACT PROJECT LAYOUT │
│ │
│ src/ │
│ ├── components/ │
│ │ ├── Button/ │
│ │ │ ├── Button.jsx ← export default Button │
│ │ │ └── Button.css │
│ │ ├── Input/ │
│ │ │ └── Input.jsx ← export default Input │
│ │ └── index.js ← barrel re-exports │
│ │ │
│ ├── pages/ │
│ │ ├── Home.jsx ← imports from components │
│ │ └── Profile.jsx │
│ │ │
│ └── App.jsx ← imports pages │
│ │
│ Each component in its own file, exported, imported where │
│ used. The dependency graph is the import graph. │
└──────────────────────────────────────────────────────────────┘
Barrel File Pattern
┌──────────────────────────────────────────────────────────────┐
│ BARREL FILE RE-EXPORTS FOR CONVENIENCE │
│ │
│ WITHOUT BARREL: │
│ import Button from "./components/Button/Button"; │
│ import Input from "./components/Input/Input"; │
│ import Card from "./components/Card/Card"; │
│ │
│ WITH BARREL: │
│ import { Button, Input, Card } from "./components"; │
│ │
│ components/index.js: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ export { default as Button } from "./Button/Button"; │ │
│ │ export { default as Input } from "./Input/Input"; │ │
│ │ export { default as Card } from "./Card/Card"; │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Trade-off: convenient imports, but the bundler resolves │
│ the entire barrel even when only one export is used. │
└──────────────────────────────────────────────────────────────┘
Circular Dependency
┌──────────────────────────────────────────────────────────────┐
│ CIRCULAR IMPORT: A IMPORTS B, B IMPORTS A │
│ │
│ A.jsx B.jsx │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ import { b } │ │ import { a } │ │
│ │ from "./B"; │ │ from "./A"; │ │
│ │ │ │ │ │
│ │ export const a = │ │ export const b = │ │
│ │ () => b(); │ │ () => a(); │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ When A is loaded first: │
│ 1. A starts evaluating │
│ 2. A imports B; B starts evaluating │
│ 3. B imports A; A is partially evaluated, a is undefined │
│ 4. B finishes with a === undefined │
│ 5. A finishes │
│ │
│ Fix: extract shared logic into C.jsx, or restructure so │
│ one direction is primary and the other receives it as a │
│ prop or parameter. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Module system | ES modules (import/export) |
| Default export | One per file; imported without braces |
| Named export | Many per file; imported with braces |
| Aliasing | import { B as C } or export { B as C } |
| Barrel file | index.js re-exporting from a folder |
| CSS import | import "./Component.css" |
| Asset import | import logo from "./logo.svg" |
| Dynamic import | const m = await import("./Modal") |
| Code splitting | lazy(() => import("./Modal")) |
| Circular dependency | Two modules importing each other |
Key takeaways:
- Every component lives in a file and is exported. Imports connect components into an application; the import graph is the dependency graph.
- Default exports fit one component per file. Imported without braces, the importer chooses the name. Conventional for page-level and single-purpose files.
- Named exports fit multiple exports per file. Imported with braces, the names must match exactly. Supports tree-shaking and refactoring.
- Match the import form to the export form. Importing a default export with braces, or a named export without braces, produces
undefined. - Barrel files provide a single entry point.
index.jsre-exports components for convenient grouped imports, at the cost of build overhead and potential circularity. - CSS and assets are imported like modules. The bundler handles the transformation and includes the result in the output.
- Dynamic imports enable code splitting.
lazyandSuspensebuild on dynamic imports to load components on demand. - Circular dependencies produce
undefined. Restructure to one-way dependencies, or extract shared code into a third module.
Remember: Exports and imports are the connective tissue of a React application. Each component is defined in its own file, exported with export default or export, and imported where it is used. The choice between default and named exports is a convention, not a rule, but consistency within a project matters more than the specific choice. Default exports read cleanly for one-component files; named exports make multiple exports per file explicit and improve refactoring. Barrel files simplify imports but add build overhead and can cause circular dependencies. The most common error is a mismatch between the export form and the import form, which produces undefined rather than an error message, making it confusing to debug. Understanding the module system, and the patterns that make it work, is the foundation of organizing a React codebase.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!