A well-organized project structure is crucial for maintainability, especially as your app grows. Mobile projects have a unique structural challenge that web projects do not: you have platform-specific native directories (ios/ and android/) alongside your JavaScript source code, configuration files for multiple build systems (Gradle, Xcode, Metro, Babel), and assets that may need different resolutions for different screen densities. Without a clear structure, navigating between these layers becomes painful fast.This module covers industry-standard patterns used by companies like Shopify, Discord, and Coinbase.What You’ll Learn:
For production apps, use a feature-based structure. The key insight: organize by business domain (auth, products, cart), not by technical role (components, screens, hooks). When a new developer joins and is asked to “fix the cart total calculation,” they should open one folder, not hunt across five. This pattern also maps naturally to team ownership — one squad owns the features/cart/ directory, another owns features/products/.A mobile-specific consideration: features often need platform-specific implementations. Rather than creating top-level ios/ and android/ source directories, use React Native’s file-extension convention (e.g., PaymentButton.ios.tsx and PaymentButton.android.tsx) within the feature folder. This keeps platform differences co-located with the feature they belong to.
Each feature is a self-contained module with everything it needs. Think of each feature folder as a mini-app: it owns its screens, components, hooks, services, types, and state. The benefit is isolation — a developer working on the “cart” feature does not need to understand the “auth” feature’s internals, and deleting a feature means removing one folder rather than hunting through a dozen shared directories.This pattern becomes especially valuable in React Native because features often have platform-specific behavior (e.g., Apple Pay on iOS vs Google Pay on Android) that stays contained within the feature folder rather than leaking into shared code.
// src/features/products/index.ts (Barrel Export)// Export only what other features need// Screens (for navigation)export { ProductsScreen } from './screens/ProductsScreen';export { ProductDetailScreen } from './screens/ProductDetailScreen';// Components (if shared)export { ProductCard } from './components/ProductCard';// Hooks (if shared)export { useProducts } from './hooks/useProducts';// Types (if shared)export type { Product, ProductCategory } from './types/product.types';// Store (for root store)export { productSlice, productReducer } from './store/productSlice';
Barrel exports simplify imports and create a clean public API for each module:
// src/shared/components/ui/index.tsexport { Button } from './Button';export { Input } from './Input';export { Text } from './Text';export { Card } from './Card';export { Avatar } from './Avatar';export { Badge } from './Badge';// Typesexport type { ButtonProps } from './Button';export type { InputProps } from './Input';
// src/shared/hooks/index.tsexport { useDebounce } from './useDebounce';export { useKeyboard } from './useKeyboard';export { useNetworkStatus } from './useNetworkStatus';export { useAppState } from './useAppState';export { usePrevious } from './usePrevious';
// Before (relative imports)import { Button } from '../../../shared/components/ui/Button';import { useAuth } from '../../auth/hooks/useAuth';// After (path aliases)import { Button } from '@/shared/components/ui';import { useAuth } from '@/features/auth';
Managing environment variables in React Native is more nuanced than in web development. On the web, tools like dotenv inject values at build time and you are done. In React Native, you need to consider: (1) JavaScript-side variables for your React code, (2) native-side variables for iOS Info.plist and Android build.gradle configurations, and (3) the fact that secrets bundled into a mobile app can be extracted by anyone who decompiles the binary. Never put truly secret API keys directly in your JS bundle — use a backend proxy instead.
# Auto detect text files and perform LF normalization* text=auto# JS/TS files*.js text eol=lf*.jsx text eol=lf*.ts text eol=lf*.tsx text eol=lf*.json text eol=lf# Config files*.yml text eol=lf*.yaml text eol=lf*.md text eol=lf# Shell scripts*.sh text eol=lf# Windows batch files*.bat text eol=crlf*.cmd text eol=crlf# Binary files*.png binary*.jpg binary*.jpeg binary*.gif binary*.ico binary*.ttf binary*.woff binary*.woff2 binary
// ✅ Goodexport function ProductCard() { }export function UserAvatar() { }export function NavigationHeader() { }// ❌ Badexport function productCard() { } // Should be PascalCaseexport function Product_Card() { } // No underscoresexport function ProductCardComponent() { } // Don't suffix with 'Component'
// ✅ Goodexport function useAuth() { }export function useProducts() { }export function useLocalStorage() { }// ❌ Badexport function Auth() { } // Must start with 'use'export function getProducts() { } // Must start with 'use'export function useAuthHook() { } // Don't suffix with 'Hook'
Structural mistakes that compound over time:Putting platform-specific code in shared directories. If shared/components/DatePicker.tsx contains Platform.OS === 'ios' ? ... branches that grow to 200+ lines, split it into DatePicker.ios.tsx and DatePicker.android.tsx. The file-extension convention keeps Metro happy and makes each platform’s code independently readable.Circular barrel exports. When features/auth/index.ts re-exports from features/auth/hooks/useAuth.ts, and that hook imports from features/products/index.ts, which imports from features/auth/index.ts — Metro’s module resolver can silently return undefined for the circular import. You will get a cryptic “X is not a function” runtime error with no useful stack trace. Break cycles by importing from the specific file, not the barrel, when a cross-feature dependency exists.Ignoring the ios/ and android/ native directories. These are not “generated files you never touch.” When you run npx pod-install or update Gradle plugin versions, files in these directories change. Commit them to version control. If a teammate’s ios/Pods/ is out of sync with yours, you will waste hours debugging build failures that have nothing to do with your JavaScript code.Storing secrets in app.json or .env files that get bundled. React Native bundles are just ZIP files containing JavaScript. Anyone can extract and read them. Use your backend as a proxy for secret API keys, and use Expo’s expo-secure-store or the native Keychain/Keystore for tokens that must live on-device.