| title | Avoid Barrel Exports |
|---|---|
| impact | CRITICAL |
| tags | bundle, imports, barrel, tree-shaking |
Refactor barrel imports (index files) to reduce bundle size and improve startup time.
Incorrect:
import { Button } from './components';
// Loads ALL exports from components/index.tsCorrect:
import Button from './components/Button';
// Loads only Button- Bundle contains unused code from libraries
- Circular dependency warnings in Metro
- Hot Module Replacement (HMR) breaks frequently
- TTI is slow due to module evaluation
// components/index.ts (barrel file)
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal';
export { Sidebar } from './Sidebar';
// Usage (barrel import)
import { Button } from './components';Metro includes all exports even if you use one:
// Only need Button, but entire barrel is bundled
import { Button } from './components';
// Card, Modal, Sidebar also included!All modules evaluate before returning your import:
import { Button } from './components';
// JavaScript must evaluate:
// - Button.tsx
// - Card.tsx
// - Modal.tsx
// - Sidebar.tsx
// Even though you only use ButtonBarrel files make cycles easier to create accidentally:
Warning: Require cycle:
components/index.ts -> Button.tsx -> utils/index.ts -> components/index.ts
Breaks HMR, causes unpredictable behavior.
Replace barrel imports with direct paths:
// BEFORE: Barrel import
import { Button, Card } from './components';
// AFTER: Direct imports
import Button from './components/Button';
import Card from './components/Card';npm install -D eslint-plugin-no-barrel-files// eslint.config.js
import noBarrelFiles from 'eslint-plugin-no-barrel-files';
export default [
{
plugins: { 'no-barrel-files': noBarrelFiles },
rules: {
'no-barrel-files/no-barrel-files': 'error',
},
},
];Enable tree shaking to automatically remove unused barrel exports.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: true,
},
});
module.exports = config;# .env
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1
EXPO_UNSTABLE_TREE_SHAKING=1npm install @rnx-kit/metro-serializer-esbuildTree shaking built-in.
// BAD: Imports entire library
import { format, addDays, isToday } from 'date-fns';
// GOOD: Direct imports
import format from 'date-fns/format';
import addDays from 'date-fns/addDays';
import isToday from 'date-fns/isToday';Some libraries provide Babel plugins:
// babel.config.js
module.exports = {
plugins: [
'react-native-paper/babel', // Auto-transforms imports
],
};Transforms:
import { Button } from 'react-native-paper';
// Into:
import Button from 'react-native-paper/lib/module/components/Button';Look for index.ts files with multiple exports:
grep -r "export \* from" src/
grep -r "export { .* } from" src/// Find all usages
// VS Code: Cmd+Shift+F for "from './components'"
// Replace each with direct import
import Button from './components/Button';If your package is consumed by others:
// Keep index.ts for package API
// components/index.ts
export { Button } from './Button';
// Internal code uses direct imports
// src/screens/Home.tsx
import Button from '../components/Button';# Use codemod or search-replace
# Find: import { (\w+) } from '\.\/components';
# Replace: import $1 from './components/$1';After refactoring:
- Run bundle analysis (see bundle-analyze-js.md)
- Compare sizes before/after
- Check for circular dependency warnings
- Breaking external consumers: If publishing a library, keep barrel for public API
- IDE auto-imports: Configure IDE to prefer direct imports
- Inconsistent patterns: Enforce with ESLint across team
- bundle-analyze-js.md - Verify impact
- bundle-tree-shaking.md - Automatic solution
- bundle-library-size.md - Check library patterns