Feature slices group everything a product area needs in one directory; layer folders (components/, hooks/, screens/ at the repo root) scatter related code across the tree. The split you pick on day one determines how painful the first 50 features feel.
Feature slices (vertical) organize by product capability - features/orders, features/inbox. Changes to Orders touch files under one subtree.
Layers (horizontal) organize by technical role - global components/, hooks/, screens/. Every feature sprinkles files across multiple top-level folders.
Expo Router app/ maps URLs to screens. Route files should stay thin entry points; heavy logic lives in features/.
index.ts public APIs enforce import boundaries - cross-feature imports use @/features/orders, never @/features/orders/components/OrderRow.
Shared components/ui/ holds design-system primitives used by many features; feature components/ holds product-specific composition.
OrderRow is not a shared primitive - duplicate or promote to ui/
lib/ and components/ui/ are the shared layer
Cross-cutting utilities and design tokens live here
// ✅ Cross-feature import through public APIimport type { Order } from "@/features/orders";// ❌ Reaching into another feature's internalsimport { OrderRow } from "@/features/orders/components/OrderRow";
Global components/ graveyard - Every one-off card lands in components/ because it is "reusable someday." Fix: Default to features/<name>/components/; promote to ui/ only after the second consumer exists.
Fat route files in app/ - Business logic, fetch calls, and modals live in app/(tabs)/orders/index.tsx. Fix: Re-export from features/orders; keep routes under ~5 lines.
Deep imports across features - features/inbox imports features/orders/components/OrderRow. Fix: Import types from @/features/orders; duplicate small presentational rows or extract shared UI to components/ui/.
No index.ts public API - Fifty files per feature with no export boundary; everything is public. Fix: Export only screens and shared types from index.ts; treat other paths as private.
Layer folders plus feature folders - screens/OrdersScreen.tsx and features/orders/screens/OrdersScreen.tsx both exist. Fix: Pick one scheme per app; hybrid confusion doubles search cost.
Shared hooks in the wrong layer - useOrders in global hooks/ but only Orders uses it. Fix: Colocate single-feature hooks under features/orders/hooks/; reserve global hooks/ for truly shared behavior (useAppTheme).
Monorepo packages without feature boundaries - Shared @repo/ui grows feature-specific props. Fix: Keep product features in the app; packages hold primitives only. See Shared Packages & Metro Resolution.
A directory that contains everything one product area needs - screens, hooks, components, types - so engineers work in one subtree. Example: src/features/orders/.
What is a layered structure?
Top-level folders grouped by technical role: components/, hooks/, screens/, services/. Every feature spreads files across these folders instead of one slice.
What goes in app/ vs features/?
app/ holds Expo Router route files - URL mapping and layout nesting. features/ holds implementation - screens, hooks, and feature components. Route files should re-export feature screens.
What goes in components/ui/ vs features/*/components/?
components/ui/ - design-system primitives used by many features (Button, Screen, TextField). features/*/components/ - product-specific UI used only inside that feature (OrderRow, InboxThread).
Why use index.ts in each feature?
It defines the public API. Other features and routes import @/features/orders - not deep paths. Internals can change without breaking consumers.
Can features import from other features?
Yes, but only through the other feature's index.ts - typically types or a single exported screen. Avoid importing internal components; that couples features silently.
When should I use layered folders instead?
Very small apps with few screens and one developer. Once PRs routinely touch multiple top-level folders for one feature, migrate to slices.
How do I migrate from layers to slices?
Pick the worst god screen. Create features/<name>/, move its screen, hooks, and components, add index.ts, and thin the route file. Repeat per feature - no big-bang rewrite required.
Where do API clients and auth live?
Cross-cutting infrastructure in src/lib/ - api.ts, storage.ts, analytics.ts. Feature hooks call into lib/; lib/ does not import from features/.
Should hooks be global or per-feature?
Per-feature when only one screen uses them (useOrders in features/orders/hooks/). Global in src/hooks/ when shared across features (useAppTheme, useSession).
Gotcha: why did our global components/ folder become unmaintainable?
Single-use wrappers accumulate because "components/" feels like the right place for any JSX file. Default to feature-local components; promote to ui/ only with a second real consumer.
How does this interact with container/presenter?
features/<name>/screens/ holds containers (data, navigation). features/<name>/components/ holds presenters (props-only UI). See Container/Presenter on Mobile.
Does Expo Router require features/?
No - Router only requires an app/ directory. features/ is an organizational convention that keeps routes thin and implementation testable.
How do path aliases like @/* work?
Expo templates ship tsconfig.json with "paths": { "@/*": ["./*"] } or "./src/*". Point @/ at src/ so @/features/orders resolves consistently. Metro respects the same paths when configured via expo/tsconfig or babel-plugin-module-resolver if needed.
What about tests - colocate or __tests__ folder?
Colocate OrderRow.test.tsx next to OrderRow.tsx inside the feature folder. Feature integration tests live in features/orders/__tests__/ if you prefer grouping. Keep tests inside the slice you are testing.