Project Structure & Organization
A well-organized project structure is crucial for maintainability and team collaboration. This guide covers best practices for structuring React Native projects with Expo.
Standard Expo Project Structure
Here's the recommended structure for a typical Expo project:
my-app/
├── app/ # App Router directory (Expo Router)
│ ├── (tabs)/ # Tab group
│ │ ├── _layout.tsx # Tab layout
│ │ ├── index.tsx # Home tab
│ │ └── explore.tsx # Explore tab
│ ├── modal.tsx # Modal screen
│ ├── +not-found.tsx # 404 page
│ └── _layout.tsx # Root layout
├── components/ # Reusable components
│ ├── ui/ # Basic UI components
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ └── Modal.tsx
│ ├── forms/ # Form-specific components
│ │ ├── LoginForm.tsx
│ │ └── ProfileForm.tsx
│ └── layout/ # Layout components
│ ├── Header.tsx
│ └── Container.tsx
├── constants/ # App constants
│ ├── Colors.ts
│ ├── Sizes.ts
│ └── Config.ts
├── hooks/ # Custom hooks
│ ├── useAuth.ts
│ ├── useApi.ts
│ └── useStorage.ts
├── services/ # External services
│ ├── api/
│ │ ├── client.ts
│ │ ├── auth.ts
│ │ └── users.ts
│ ├── storage/
│ │ └── asyncStorage.ts
│ └── notifications/
│ └── expo-notifications.ts
├── types/ # TypeScript type definitions
│ ├── api.ts
│ ├── navigation.ts
│ └── user.ts
├── utils/ # Utility functions
│ ├── format.ts
│ ├── validation.ts
│ └── helpers.ts
├── assets/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── icons/
├── __tests__/ # Test files
│ ├── components/
│ ├── utils/
│ └── __mocks__/
├── app.json # Expo configuration
├── package.json
├── tsconfig.json
├── expo-env.d.ts
└── README.md
File Naming Conventions
Consistent naming conventions improve code readability and team collaboration:
Components
✅ Recommended:
- PascalCase for component files: `UserProfile.tsx`
- Match component name with file name
- Use descriptive names: `ProductCard.tsx` not `Card.tsx`
❌ Avoid:
- camelCase: `userProfile.tsx`
- kebab-case: `user-profile.tsx`
- Generic names: `Item.tsx`, `Component.tsx`
Hooks
✅ Recommended:
- camelCase starting with 'use': `useUserProfile.ts`
- Descriptive and specific: `useAuthToken.ts`
❌ Avoid:
- Missing 'use' prefix: `authToken.ts`
- Generic names: `useData.ts`
Utilities and Services
✅ Recommended:
- camelCase for functions: `formatCurrency.ts`
- Descriptive names: `apiClient.ts`, `storageService.ts`
❌ Avoid:
- PascalCase: `FormatCurrency.ts`
- Abbreviations: `apiC.ts`, `utils.ts`
Types and Interfaces
✅ Recommended:
- PascalCase: `User.ts`, `ApiResponse.ts`
- Descriptive: `UserProfile.ts`, `AuthToken.ts`
- Group related types: `api/` folder for API types
❌ Avoid:
- camelCase: `user.ts`
- Generic names: `types.ts`, `interfaces.ts`
Component Organization Patterns
Atomic Design Structure
Organize components using atomic design principles:
components/
├── atoms/ # Basic building blocks
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ ├── Button.stories.tsx
│ │ └── index.ts
│ ├── Input/
│ └── Text/
├── molecules/ # Simple combinations
│ ├── SearchBox/
│ ├── FormField/
│ └── ListItem/
├── organisms/ # Complex components
│ ├── Header/
│ ├── ProductList/
│ └── UserProfile/
└── templates/ # Page layouts
├── AuthLayout/
└── DashboardLayout/
Feature-Based Organization
For larger apps, consider organizing by features:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── SignupForm.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── services/
│ │ │ └── authApi.ts
│ │ ├── types/
│ │ │ └── auth.ts
│ │ └── index.ts
│ ├── profile/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── services/
│ └── dashboard/
├── shared/ # Shared across features
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── types/
└── app/ # App Router files
Asset Management
Image Organization
assets/
├── images/
│ ├── icons/ # App icons and small graphics
│ │ ├── user-icon.png
│ │ ├── user-icon@2x.png # High DPI version
│ │ └── user-icon@3x.png # Extra high DPI
│ ├── illustrations/ # Larger graphics
│ │ ├── onboarding-1.png
│ │ └── empty-state.png
│ └── backgrounds/ # Background images
│ └── splash-bg.png
├── fonts/ # Custom fonts
│ ├── Inter-Regular.ttf
│ ├── Inter-Bold.ttf
│ └── Inter-SemiBold.ttf
└── data/ # Static data files
├── countries.json
└── categories.json
Asset Import Patterns
// ✅ Centralized asset imports
// assets/index.ts
export const Images = {
userIcon: require("./images/icons/user-icon.png"),
onboardingIllustration: require("./images/illustrations/onboarding-1.png"),
splashBackground: require("./images/backgrounds/splash-bg.png"),
}
export const Fonts = {
regular: "Inter-Regular",
bold: "Inter-Bold",
semiBold: "Inter-SemiBold",
}
// Usage in components
import { Images, Fonts } from "@/assets"
function ProfileScreen() {
return (
<View>
<Image source={Images.userIcon} />
<Text style={{ fontFamily: Fonts.semiBold }}>Profile</Text>
</View>
)
}
Configuration Management
Environment-Specific Configuration
// constants/Config.ts
const Config = {
API_URL: process.env.EXPO_PUBLIC_API_URL || "https://api.example.com",
APP_NAME: "MyApp",
VERSION: "1.0.0",
IS_DEV: __DEV__,
SENTRY_DSN: process.env.EXPO_PUBLIC_SENTRY_DSN,
// Feature flags
FEATURES: {
PUSH_NOTIFICATIONS: true,
ANALYTICS: !__DEV__,
DEBUG_MENU: __DEV__,
},
}
export default Config
App Configuration (app.json)
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.company.myapp",
"infoPlist": {
"NSCameraUsageDescription": "This app uses the camera to scan QR codes."
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.company.myapp",
"permissions": ["CAMERA", "RECORD_AUDIO"]
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
}
]
],
"experiments": {
"typedRoutes": true
}
}
}
Type Definitions
Navigation Types
// types/navigation.ts
import { NavigatorScreenParams } from "@react-navigation/native"
export type RootStackParamList = {
"(tabs)": NavigatorScreenParams<TabParamList>
modal: { userId: string }
"user/[id]": { id: string }
}
export type TabParamList = {
index: undefined
explore: undefined
profile: { userId?: string }
}
// Augment the module for type safety
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
API Types
// types/api.ts
export interface ApiResponse<T = any> {
data: T
message: string
success: boolean
errors?: string[]
}
export interface User {
id: string
email: string
name: string
avatar?: string
createdAt: string
updatedAt: string
}
export interface AuthTokens {
accessToken: string
refreshToken: string
expiresIn: number
}
// API endpoint types
export interface LoginRequest {
email: string
password: string
}
export interface LoginResponse
extends ApiResponse<{
user: User
tokens: AuthTokens
}> {}
Testing Structure
Test Organization
__tests__/
├── components/ # Component tests
│ ├── ui/
│ │ ├── Button.test.tsx
│ │ └── Input.test.tsx
│ └── forms/
│ └── LoginForm.test.tsx
├── hooks/ # Hook tests
│ ├── useAuth.test.ts
│ └── useApi.test.ts
├── services/ # Service tests
│ ├── api/
│ │ └── authApi.test.ts
│ └── storage/
│ └── asyncStorage.test.ts
├── utils/ # Utility tests
│ ├── format.test.ts
│ └── validation.test.ts
├── __mocks__/ # Mock files
│ ├── @expo/
│ ├── react-native/
│ └── api/
└── setup/ # Test setup
├── jest-setup.ts
└── test-utils.tsx
Test Configuration
// __tests__/setup/test-utils.tsx
import React from "react"
import { render } from "@testing-library/react-native"
import { NavigationContainer } from "@react-navigation/native"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
// Custom render function with providers
export function renderWithProviders(
ui: React.ReactElement,
options?: {
queryClient?: QueryClient
navigationOptions?: any
}
) {
const queryClient =
options?.queryClient ||
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<NavigationContainer>{children}</NavigationContainer>
</QueryClientProvider>
)
}
return render(ui, { wrapper: Wrapper, ...options })
}
// Re-export everything from react-testing-library
export * from "@testing-library/react-native"
Development Scripts
Package.json Scripts
{
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"type-check": "tsc --noEmit",
"build:android": "eas build --platform android",
"build:ios": "eas build --platform ios",
"build:all": "eas build --platform all",
"submit:android": "eas submit --platform android",
"submit:ios": "eas submit --platform ios",
"update": "eas update",
"doctor": "expo doctor",
"clean": "expo r -c"
}
}
Best Practices Summary
- Consistency: Follow established naming conventions throughout the project
- Modularity: Keep components small and focused on single responsibilities
- Type Safety: Use TypeScript extensively for better development experience
- Testing: Organize tests to mirror your source code structure
- Asset Optimization: Use appropriate image formats and sizes for mobile
- Configuration: Centralize configuration and use environment variables
- Documentation: Keep README files updated and document complex components
Next Steps
- Development Fundamentals - Learn component patterns and styling
- Navigation & Routing - Master app navigation with Expo Router
- Testing Strategies - Comprehensive testing approaches