Import Rules & Path Aliases
The leaf-import rule
When one feature needs something from another, import the leaf module directly — never the feature’s barrel.
// ✅ from another feature — import the leaf module
import { useOrders } from "@features/orders/hooks/useOrders";
// ❌ pulls the whole feature graph and risks import cycles
import { useOrders } from "@features/orders";
Only app/ route files import a feature’s barrel (@features/orders),
because routes sit at the top of the graph and can’t create a cycle.
Why it matters: a barrel re-exports everything in a feature, including its screens. If feature A imports feature B’s barrel, and B’s barrel pulls in a screen that imports from A, you get a cycle that shows up as a confusing “undefined is not a function” at runtime. Leaf imports avoid the whole class of problem.
Path aliases (declared in three places)
Aliases keep imports readable. They must be declared in sync in three files — Babel resolves them at build time, TypeScript for editor/type-check, and Jest for tests:
// babel.config.js
plugins: [
["module-resolver", {
alias: { "@": "./src", "@features": "./src/features", "@shared": "./src/shared" },
}],
];
// tsconfig.json
"paths": {
"@/*": ["src/*"],
"@features/*": ["src/features/*"],
"@shared/*": ["src/shared/*"]
}
// jest.config.js
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"^@features/(.*)$": "<rootDir>/src/features/$1",
"^@shared/(.*)$": "<rootDir>/src/shared/$1",
}
If you add or rename an alias, update all three files. Miss one and builds, the type-checker, or tests will disagree about where a module lives.