Module Overview
Estimated Time: 2 hours | Difficulty: Beginner-Intermediate | Prerequisites: Environment setup complete
- Default project structure (Expo and CLI)
- Feature-based folder organization
- Barrel exports and module organization
- Configuration management
- Environment variables
- Path aliases and absolute imports
Default Project Structures
Expo Project Structure
Copy
my-expo-app/
├── app/ # Expo Router screens (file-based routing)
│ ├── (tabs)/ # Tab group
│ │ ├── _layout.tsx # Tab bar configuration
│ │ ├── index.tsx # Home tab (/)
│ │ └── explore.tsx # Explore tab (/explore)
│ ├── _layout.tsx # Root layout
│ ├── +not-found.tsx # 404 screen
│ └── modal.tsx # Modal screen
├── assets/ # Static assets
│ ├── fonts/ # Custom fonts
│ └── images/ # Images and icons
├── components/ # Reusable components
│ ├── ui/ # UI primitives
│ └── ThemedText.tsx
├── constants/ # App constants
│ └── Colors.ts
├── hooks/ # Custom hooks
│ └── useColorScheme.ts
├── .expo/ # Expo cache (gitignored)
├── node_modules/ # Dependencies (gitignored)
├── app.json # Expo configuration
├── babel.config.js # Babel configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── .gitignore
React Native CLI Structure
Copy
my-cli-app/
├── android/ # Android native code
│ ├── app/
│ │ ├── src/main/
│ │ │ ├── java/ # Java/Kotlin code
│ │ │ └── res/ # Android resources
│ │ └── build.gradle
│ ├── gradle/
│ └── settings.gradle
├── ios/ # iOS native code
│ ├── MyApp/
│ │ ├── AppDelegate.mm
│ │ ├── Info.plist
│ │ └── Images.xcassets
│ ├── MyApp.xcodeproj
│ ├── MyApp.xcworkspace
│ └── Podfile
├── src/ # Your app code
│ ├── components/
│ ├── screens/
│ └── App.tsx
├── __tests__/ # Test files
├── node_modules/
├── .bundle/
├── index.js # Entry point
├── app.json
├── babel.config.js
├── metro.config.js
├── package.json
└── tsconfig.json
Recommended Enterprise Structure
For production apps, use a feature-based structure:Copy
src/
├── app/ # App entry and providers
│ ├── App.tsx # Root component
│ ├── providers/ # Context providers
│ │ ├── AuthProvider.tsx
│ │ ├── ThemeProvider.tsx
│ │ └── index.tsx
│ └── navigation/ # Navigation configuration
│ ├── RootNavigator.tsx
│ ├── AuthNavigator.tsx
│ ├── MainNavigator.tsx
│ └── linking.ts
│
├── features/ # Feature modules
│ ├── auth/ # Authentication feature
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── SignupForm.tsx
│ │ ├── screens/
│ │ │ ├── LoginScreen.tsx
│ │ │ ├── SignupScreen.tsx
│ │ │ └── ForgotPasswordScreen.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── services/
│ │ │ └── authService.ts
│ │ ├── store/
│ │ │ └── authSlice.ts
│ │ ├── types/
│ │ │ └── auth.types.ts
│ │ └── index.ts # Barrel export
│ │
│ ├── products/ # Products feature
│ │ ├── components/
│ │ │ ├── ProductCard.tsx
│ │ │ ├── ProductList.tsx
│ │ │ └── ProductFilters.tsx
│ │ ├── screens/
│ │ │ ├── ProductsScreen.tsx
│ │ │ └── ProductDetailScreen.tsx
│ │ ├── hooks/
│ │ │ ├── useProducts.ts
│ │ │ └── useProductFilters.ts
│ │ ├── services/
│ │ │ └── productService.ts
│ │ ├── store/
│ │ │ └── productSlice.ts
│ │ ├── types/
│ │ │ └── product.types.ts
│ │ └── index.ts
│ │
│ ├── cart/ # Cart feature
│ │ ├── components/
│ │ ├── screens/
│ │ ├── hooks/
│ │ ├── store/
│ │ └── index.ts
│ │
│ └── profile/ # Profile feature
│ ├── components/
│ ├── screens/
│ ├── hooks/
│ └── index.ts
│
├── shared/ # Shared code across features
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # Primitive components
│ │ │ ├── Button.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Text.tsx
│ │ │ ├── Card.tsx
│ │ │ └── index.ts
│ │ ├── layout/ # Layout components
│ │ │ ├── Container.tsx
│ │ │ ├── SafeArea.tsx
│ │ │ └── index.ts
│ │ └── feedback/ # Feedback components
│ │ ├── Loading.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── Toast.tsx
│ │ └── index.ts
│ │
│ ├── hooks/ # Shared hooks
│ │ ├── useDebounce.ts
│ │ ├── useKeyboard.ts
│ │ ├── useNetworkStatus.ts
│ │ └── index.ts
│ │
│ ├── services/ # Shared services
│ │ ├── api/
│ │ │ ├── client.ts # API client (Axios/fetch)
│ │ │ ├── interceptors.ts
│ │ │ └── index.ts
│ │ ├── storage/
│ │ │ ├── secureStorage.ts
│ │ │ ├── asyncStorage.ts
│ │ │ └── index.ts
│ │ └── analytics/
│ │ └── analytics.ts
│ │
│ ├── utils/ # Utility functions
│ │ ├── formatting.ts
│ │ ├── validation.ts
│ │ ├── date.ts
│ │ └── index.ts
│ │
│ ├── constants/ # App constants
│ │ ├── colors.ts
│ │ ├── spacing.ts
│ │ ├── typography.ts
│ │ ├── config.ts
│ │ └── index.ts
│ │
│ └── types/ # Shared TypeScript types
│ ├── navigation.types.ts
│ ├── api.types.ts
│ ├── common.types.ts
│ └── index.ts
│
├── assets/ # Static assets
│ ├── fonts/
│ ├── images/
│ ├── icons/
│ └── animations/ # Lottie files
│
└── __tests__/ # Test utilities
├── setup.ts
├── mocks/
└── utils/
Feature Module Pattern
Each feature is a self-contained module with everything it needs:Copy
// 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';
Feature Structure Example
Copy
// src/features/products/types/product.types.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
images: string[];
category: ProductCategory;
inStock: boolean;
rating: number;
reviewCount: number;
}
export interface ProductCategory {
id: string;
name: string;
slug: string;
}
export interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
sortBy?: 'price' | 'rating' | 'newest';
}
Copy
// src/features/products/services/productService.ts
import { apiClient } from '@/shared/services/api';
import type { Product, ProductFilters } from '../types/product.types';
export const productService = {
async getProducts(filters?: ProductFilters): Promise<Product[]> {
const response = await apiClient.get('/products', { params: filters });
return response.data;
},
async getProductById(id: string): Promise<Product> {
const response = await apiClient.get(`/products/${id}`);
return response.data;
},
async searchProducts(query: string): Promise<Product[]> {
const response = await apiClient.get('/products/search', {
params: { q: query },
});
return response.data;
},
};
Copy
// src/features/products/hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
import { productService } from '../services/productService';
import type { ProductFilters } from '../types/product.types';
export function useProducts(filters?: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => productService.getProducts(filters),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useProduct(id: string) {
return useQuery({
queryKey: ['product', id],
queryFn: () => productService.getProductById(id),
enabled: !!id,
});
}
Copy
// src/features/products/components/ProductCard.tsx
import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import type { Product } from '../types/product.types';
interface ProductCardProps {
product: Product;
onPress: (product: Product) => void;
}
export function ProductCard({ product, onPress }: ProductCardProps) {
return (
<Pressable
style={styles.container}
onPress={() => onPress(product)}
>
<Image
source={{ uri: product.images[0] }}
style={styles.image}
resizeMode="cover"
/>
<View style={styles.content}>
<Text style={styles.name} numberOfLines={2}>
{product.name}
</Text>
<Text style={styles.price}>
${product.price.toFixed(2)}
</Text>
<View style={styles.rating}>
<Text style={styles.ratingText}>
⭐ {product.rating} ({product.reviewCount})
</Text>
</View>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
image: {
width: '100%',
height: 150,
},
content: {
padding: 12,
},
name: {
fontSize: 16,
fontWeight: '600',
color: '#1f2937',
marginBottom: 4,
},
price: {
fontSize: 18,
fontWeight: '700',
color: '#3b82f6',
marginBottom: 4,
},
rating: {
flexDirection: 'row',
alignItems: 'center',
},
ratingText: {
fontSize: 14,
color: '#6b7280',
},
});
Barrel Exports
Barrel exports simplify imports and create a clean public API for each module:Copy
// src/shared/components/ui/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Text } from './Text';
export { Card } from './Card';
export { Avatar } from './Avatar';
export { Badge } from './Badge';
// Types
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
Copy
// src/shared/hooks/index.ts
export { useDebounce } from './useDebounce';
export { useKeyboard } from './useKeyboard';
export { useNetworkStatus } from './useNetworkStatus';
export { useAppState } from './useAppState';
export { usePrevious } from './usePrevious';
Using Barrel Exports
Copy
// Clean imports from barrel exports
import { Button, Input, Card } from '@/shared/components/ui';
import { useDebounce, useNetworkStatus } from '@/shared/hooks';
import { ProductCard, useProducts } from '@/features/products';
// Instead of:
import { Button } from '@/shared/components/ui/Button';
import { Input } from '@/shared/components/ui/Input';
import { Card } from '@/shared/components/ui/Card';
Path Aliases
Configure path aliases for cleaner imports:TypeScript Configuration
Copy
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/app/*": ["src/app/*"],
"@/features/*": ["src/features/*"],
"@/shared/*": ["src/shared/*"],
"@/assets/*": ["src/assets/*"]
}
}
}
Babel Configuration
Copy
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'], // or 'module:metro-react-native-babel-preset'
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@': './src',
'@/app': './src/app',
'@/features': './src/features',
'@/shared': './src/shared',
'@/assets': './src/assets',
},
},
],
],
};
};
Copy
npm install --save-dev babel-plugin-module-resolver
Usage
Copy
// 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';
Environment Variables
Using Expo
Copy
# .env
API_URL=https://api.example.com
API_KEY=your-api-key
ENVIRONMENT=development
Copy
// app.config.ts
import 'dotenv/config';
export default {
expo: {
name: 'My App',
slug: 'my-app',
extra: {
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
environment: process.env.ENVIRONMENT,
},
},
};
Copy
// src/shared/constants/config.ts
import Constants from 'expo-constants';
export const config = {
apiUrl: Constants.expoConfig?.extra?.apiUrl ?? 'https://api.example.com',
apiKey: Constants.expoConfig?.extra?.apiKey ?? '',
environment: Constants.expoConfig?.extra?.environment ?? 'development',
isDev: __DEV__,
isProd: !__DEV__,
};
Using React Native CLI
Copy
# Install react-native-config
npm install react-native-config
cd ios && pod install
Copy
# .env
API_URL=https://api.example.com
API_KEY=your-api-key
# .env.staging
API_URL=https://staging-api.example.com
API_KEY=staging-api-key
# .env.production
API_URL=https://api.example.com
API_KEY=production-api-key
Copy
// src/shared/constants/config.ts
import Config from 'react-native-config';
export const config = {
apiUrl: Config.API_URL ?? 'https://api.example.com',
apiKey: Config.API_KEY ?? '',
isDev: __DEV__,
isProd: !__DEV__,
};
Environment-Specific Builds
Copy
// package.json
{
"scripts": {
"start": "expo start",
"start:staging": "ENVFILE=.env.staging expo start",
"start:prod": "ENVFILE=.env.production expo start",
"build:staging": "eas build --profile staging",
"build:prod": "eas build --profile production"
}
}
Configuration Files
ESLint Configuration
Copy
// .eslintrc.js
module.exports = {
root: true,
extends: [
'expo',
'@react-native',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
plugins: ['@typescript-eslint', 'react-hooks', 'import'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2021,
sourceType: 'module',
},
rules: {
// TypeScript
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
// React
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
'import/no-duplicates': 'error',
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
};
Prettier Configuration
Copy
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
TypeScript Configuration
Copy
// tsconfig.json
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-native",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": ["node_modules", "babel.config.js", "metro.config.js"]
}
Git Configuration
.gitignore
Copy
# Dependencies
node_modules/
.pnp/
.pnp.js
# Expo
.expo/
dist/
web-build/
# Native builds
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
# iOS
ios/Pods/
ios/build/
ios/*.xcworkspace/xcuserdata/
# Android
android/.gradle/
android/app/build/
android/build/
*.apk
*.aab
# Metro
.metro-health-check*
# Testing
coverage/
# Environment
.env
.env.*
!.env.example
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
*.log
.cache/
.gitattributes
Copy
# 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
Naming Conventions
Files and Folders
Copy
✅ Good naming:
src/
├── features/
│ └── user-profile/ # kebab-case for folders
│ ├── UserProfile.tsx # PascalCase for components
│ ├── useUserProfile.ts # camelCase with 'use' prefix for hooks
│ ├── userProfile.types.ts # camelCase with .types suffix
│ └── userProfile.utils.ts # camelCase with .utils suffix
❌ Bad naming:
src/
├── Features/ # Don't use PascalCase for folders
│ └── UserProfile/
│ ├── user-profile.tsx # Don't use kebab-case for components
│ ├── UserProfileHook.ts # Don't suffix hooks with 'Hook'
Component Naming
Copy
// ✅ Good
export function ProductCard() { }
export function UserAvatar() { }
export function NavigationHeader() { }
// ❌ Bad
export function productCard() { } // Should be PascalCase
export function Product_Card() { } // No underscores
export function ProductCardComponent() { } // Don't suffix with 'Component'
Hook Naming
Copy
// ✅ Good
export function useAuth() { }
export function useProducts() { }
export function useLocalStorage() { }
// ❌ Bad
export function Auth() { } // Must start with 'use'
export function getProducts() { } // Must start with 'use'
export function useAuthHook() { } // Don't suffix with 'Hook'
Type Naming
Copy
// ✅ Good
interface User { }
interface ProductCardProps { }
type AuthState = { }
type ProductCategory = 'electronics' | 'clothing';
// ❌ Bad
interface IUser { } // Don't prefix with 'I'
interface UserInterface { } // Don't suffix with 'Interface'
type TAuthState = { } // Don't prefix with 'T'
Quick Reference
Project Structure Checklist
Next Steps
Module 4: TypeScript in React Native
Learn TypeScript patterns and best practices for React Native development