Barrel Exports and Index Files in React
A barrel export is an index.js file that re-exports everything from a folder, allowing clean imports. Instead of import Button from './components/Button/Button', you write import { Button } from './components'. Barrel exports hide internal structure, enforce a public API, and make imports terse. But overused, they hide where code lives and make bundlers work harder. This article teaches you when to use barrel exports, how to structure them, and the pitfalls to avoid.
What Is a Barrel Export?
A barrel export is an index.js (or index.ts) file that re-exports from subfolders.
// src/components/Button/Button.jsx
export default function Button({ children, ...props }) {
return <button {...props}>{children}</button>;
}
// src/components/index.js (barrel export)
export { default as Button } from './Button/Button';
export { default as Card } from './Card/Card';
export { default as Input } from './Input/Input';
// Usage: clean import
import { Button, Card, Input } from './components';
// Without barrel: verbose import
import Button from './components/Button/Button';
import Card from './components/Card/Card';
import Input from './components/Input/Input';
Barrel exports reduce import verbosity, enforce naming consistency, and make refactoring easier: you can reorganize internal structure without breaking imports.
When to Use Barrel Exports
Good: Exporting a Folder of Components
src/components/
Button/
Button.jsx
Card/
Card.jsx
Input/
Input.jsx
index.js # barrel: re-exports all
// src/components/index.js
export { Button } from './Button/Button';
export { Card } from './Card/Card';
export { Input } from './Input/Input';
Usage is clean:
import { Button, Card, Input } from './components';
Good use case: design systems, shared libraries, and commonly-used components. Anything that other parts of the codebase reuse.
Good: Exporting a Shared Library
packages/ui-library/
src/
Button/
Button.tsx
Card/
Card.tsx
Modal/
Modal.tsx
index.ts # barrel: main export
package.json
// src/index.ts
export { Button } from './Button/Button';
export { Card } from './Card/Card';
export { Modal } from './Modal/Modal';
Consumers:
import { Button, Modal } from '@myorg/ui-library';
The barrel export is the public API. Internals are hidden.
Good: Exporting All Hooks from a Folder
src/hooks/
useAuth.js
useFetch.js
useLocalStorage.js
index.js # barrel
// src/hooks/index.js
export { useAuth } from './useAuth';
export { useFetch } from './useFetch';
export { useLocalStorage } from './useLocalStorage';
Usage:
import { useAuth, useFetch, useLocalStorage } from './hooks';
Bad: Exporting from Deeply Nested Folders
src/
features/
auth/
components/
LoginForm/
LoginForm.jsx
SignUpForm/
SignUpForm.jsx
index.js # barrel 1
hooks/
useAuth.js
index.js # barrel 2
index.js # barrel 3 (re-exports barrels 1 and 2)
This hides structure. Is LoginForm public or internal? Are useAuth public or internal? With three levels of barrels, it's unclear.
Better: don't barrel. Import directly.
// Clearer: you see the structure
import { LoginForm } from './features/auth/components/LoginForm/LoginForm';
import { useAuth } from './features/auth/hooks/useAuth';
Bad: Exporting from Feature Folders
src/
features/
auth/
components/
LoginForm.jsx
SignUpForm.jsx
hooks/
useAuth.js
services/
authService.js
pages/
LoginPage.jsx
index.js # ANTI-PATTERN: barrels everything
// src/features/auth/index.js
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export { authService } from './services/authService';
export { LoginPage } from './pages/LoginPage';
This is an anti-pattern: it hides what's internal and what's public. A component shouldn't import auth's pages.
Better: no barrel. Import what you need.
// Better: explicit imports show intent
import { useAuth } from '../features/auth/hooks/useAuth';
import { LoginForm } from '../features/auth/components/LoginForm';
// Never import pages directly
// (pages are route entry points, not reusable components)
Barrel Export Patterns
Pattern 1: Simple Barrel (Re-export Everything)
// src/components/index.js
export { Button } from './Button/Button';
export { Card } from './Card/Card';
export { Input } from './Input/Input';
export { Modal } from './Modal/Modal';
Simple, readable, works for small folders (under 20 items).
Pattern 2: Namespace Barrel (Group by Category)
For large libraries, namespace exports:
// packages/ui-library/src/index.ts
// Inputs
export { Button } from './Button/Button';
export { Input } from './Input/Input';
export { Checkbox } from './Checkbox/Checkbox';
// Display
export { Card } from './Card/Card';
export { Modal } from './Modal/Modal';
export { Tooltip } from './Tooltip/Tooltip';
// Types
export type { ButtonProps } from './Button/Button';
export type { CardProps } from './Card/Card';
Or with namespaces:
// packages/ui-library/src/index.ts
import * as Inputs from './inputs';
import * as Display from './display';
export { Inputs, Display };
// Usage
import { Inputs, Display } from '@myorg/ui-library';
const form = (
<div>
<Inputs.TextInput />
<Display.Modal>...</Display.Modal>
</div>
);
Pattern 3: Barrels with Type Exports (TypeScript)
// src/components/Button/Button.tsx
export interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
export function Button({ variant, size, children }: ButtonProps) {
// ...
}
// src/components/index.ts
export { Button } from './Button/Button';
export type { ButtonProps } from './Button/Button';
Usage:
import { Button, type ButtonProps } from './components';
const MyButton: ButtonProps = { children: 'Click me' };
The type keyword ensures the import is only for types, not runtime code. This improves tree-shaking and signals intent.
Pitfalls and How to Avoid Them
Pitfall 1: Circular Dependencies Through Barrels
If two folders have barrels and both import from each other, circular dependencies can form:
// src/components/index.js
export { Button } from './Button/Button';
export { Form } from './Form/Form'; // imports Input from ../hooks
// src/hooks/index.js
export { useFormValidation } from './useFormValidation'; // imports Button
Avoid by: don't barrel feature folders. Only barrel cohesive, non-overlapping collections (components, hooks at the same level).
Pitfall 2: Barrels Hide Where Code Lives
// Bad: you don't know where this comes from
import { Something } from './index'; // where is Something?
// Better: explicit path
import { Something } from './features/auth/hooks/useAuth';
Explicit imports make refactoring easier: changing the file path is obvious.
Pitfall 3: Barrels Slow Down Bundling
Every export { X } from './X' statement adds a module to the bundle. If you barrel 100 items and only use 3, you're bundling 97 unused exports.
Modern bundlers (webpack 5+, Vite) support tree-shaking: remove unused exports at build time. But older bundlers or misconfigured builds can bloat bundles.
Verify with:
npm install --save-dev webpack-bundle-analyzer
Pitfall 4: Over-Barrels (Barrels of Barrels)
// BAD: three levels of barrels
src/index.js imports from src/features/index.js
src/features/index.js imports from src/features/auth/index.js
src/features/auth/index.js imports from src/features/auth/components/Button.jsx
Only use one level. Import directly from the deepest level.
Best Practices for Barrel Exports
Rule 1: Barrel only cohesive collections. Barrel components/, hooks/, utils/, not feature folders.
Rule 2: Keep barrels shallow. At most one level: src/components/index.js re-exports from src/components/*/, never src/components/*/subdir/index.js.
Rule 3: Explicitly list exports. Don't use export * from './X'; it's opaque. List each export.
// GOOD: clear what's exported
export { Button } from './Button';
export { Card } from './Card';
// BAD: unclear what's exported
export * from './Button';
export * from './Card';
Rule 4: Separate public and private. Mark private items with a leading underscore.
// src/components/index.js
export { Button } from './Button';
export { Card } from './Card';
// _InternalHelper is private, not exported
Rule 5: Re-export exact names. Don't rename during export.
// GOOD: Button is Button
export { Button } from './Button';
// BAD: renames confuse readers
export { Button as CustomButton } from './Button';
Rule 6: Never import from a barrel in its own folder.
// BAD: components/Button/Button.jsx imports from components/index.js
import { Card } from '../index'; // circular risk
// GOOD: import directly
import { Card } from '../Card/Card';
Example: A Well-Structured Monorepo with Barrels
packages/ui-components/
src/
Button/
Button.tsx
Button.css
Button.test.tsx
index.ts
Card/
Card.tsx
Card.css
index.ts
Input/
Input.tsx
Input.css
index.ts
index.ts # barrel: exports all components
dist/
package.json
packages/app/
src/
pages/
Dashboard.tsx
Login.tsx
features/
auth/
hooks/
useAuth.ts
services/
authService.ts
(no barrel: import directly)
payments/
(no barrel: import directly)
index.tsx
package.json
// packages/app/src/pages/Dashboard.tsx
import { Button, Card, Input } from '@myorg/ui-components'; // barrel
import { useAuth } from '../features/auth/hooks/useAuth'; // direct import
import { authService } from '../features/auth/services/authService'; // direct import
Key Takeaways
- Barrel exports simplify imports and enforce public APIs for reusable collections.
- Use barrels for
components/,hooks/,utils/, and shared libraries. - Never barrel feature folders or create multiple levels of barrels.
- Explicitly list exports; don't use wildcard
export *. - Keep imports explicit to show code structure and prevent circular dependencies.
Frequently Asked Questions
Should I barrel every component folder?
No. Barrel only when a folder has many exports and is used by many other parts of the codebase. A small, internal folder doesn't need a barrel. Start without barrels; add them when imports get verbose.
Is export * from './X' bad?
Yes. It's opaque: readers don't know what's exported, and bundlers can't tree-shake. Use explicit export { X } statements.
Should the main package export a barrel?
Yes, if it's a library. packages/ui-components/src/index.ts should barrel all components. This is the public API. App code inside packages/app/ shouldn't barrel; import directly.
How do I deprecate a component?
Export it but mark it as deprecated in the barrel comment:
// src/components/index.ts
/**
* @deprecated Use Button2 instead
*/
export { OldButton } from './OldButton/OldButton';
export { Button2 } from './Button2/Button2';
Or use a deprecation helper:
// src/utils/deprecate.ts
export function deprecated(name: string) {
console.warn(`${name} is deprecated`);
}
// src/components/index.ts
export { default as OldButton } from './OldButton/OldButton';
// Usage triggers warning
import { OldButton } from './components';
deprecated('OldButton');
Can I use barrels with dynamic imports?
Yes, but they work less well:
const Button = await import('./components').then(m => m.Button);
Dynamic imports don't benefit from barrels' readability. Use barrels for static imports.