Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mubbi/7061514f18354638bc8857c274e2476e to your computer and use it in GitHub Desktop.

Select an option

Save mubbi/7061514f18354638bc8857c274e2476e to your computer and use it in GitHub Desktop.

React Native Architecture for Production

A production-ready, feature-modular, clean-architecture design for large React Native apps supporting multi-team development, offline-first capabilities, and maintainable scaling over years.


Dependency Flow

UI (features/) → domain/ → data/ → infrastructure/
     ↓                ↑
   shared/         (interfaces only)

The golden rule: Dependencies point inward. domain/ knows nothing about data/, infrastructure/, or features/. The data/ layer implements domain interfaces. features/ depend on domain/ (use cases, entities) — never directly on data/.


Folder Structure

src/
├── app/
│   ├── navigation/
│   │   ├── RootNavigator.tsx
│   │   ├── AuthStack.tsx
│   │   ├── MainTabs.tsx
│   │   ├── ModalStack.tsx
│   │   └── linking.ts                    # Deep link configuration
│   ├── providers/
│   │   ├── AppProviders.tsx              # Composes all providers in correct order
│   │   ├── AuthProvider.tsx
│   │   ├── ThemeProvider.tsx
│   │   ├── QueryProvider.tsx             # React Query client + config
│   │   └── ErrorBoundaryProvider.tsx
│   ├── store/
│   │   ├── rootStore.ts
│   │   └── hydration.ts                 # Store rehydration from persisted state
│   ├── config/
│   │   ├── env.ts                        # Environment-specific config (dev/staging/prod)
│   │   ├── featureFlags.ts               # Feature flag definitions + remote config
│   │   └── appConfig.ts                  # App-wide constants (timeouts, retry limits, etc.)
│   └── bootstrap.tsx                     # App initialization sequence
│
├── features/
│   ├── auth/
│   │   ├── screens/
│   │   │   ├── LoginScreen.tsx
│   │   │   └── __tests__/
│   │   │       └── LoginScreen.test.tsx
│   │   ├── components/
│   │   │   └── AuthForm.tsx
│   │   ├── hooks/
│   │   │   └── useLogin.ts
│   │   ├── viewmodels/
│   │   │   ├── LoginViewModel.ts
│   │   │   └── __tests__/
│   │   │       └── LoginViewModel.test.ts
│   │   ├── navigation/
│   │   │   └── AuthNavigator.tsx
│   │   └── index.ts                      # Barrel export — public API of the module
│   │
│   ├── users/
│   │   ├── screens/
│   │   │   └── UserListScreen.tsx
│   │   ├── components/
│   │   │   └── UserCard.tsx
│   │   ├── hooks/
│   │   │   └── useUsers.ts
│   │   ├── viewmodels/
│   │   │   └── UserListViewModel.ts
│   │   ├── navigation/
│   │   │   └── UsersNavigator.tsx
│   │   └── index.ts
│   │
│   └── messages/
│       ├── screens/
│       │   └── ChatScreen.tsx
│       ├── components/
│       │   └── MessageBubble.tsx
│       ├── hooks/
│       │   └── useMessages.ts
│       ├── viewmodels/
│       │   └── ChatViewModel.ts
│       └── index.ts
│
├── domain/
│   ├── entities/
│   │   └── User.ts
│   ├── repositories/
│   │   └── UserRepository.ts             # Interface only — no implementation
│   ├── services/
│   │   └── AuthService.ts                # Interface only
│   ├── usecases/
│   │   ├── GetUsersUseCase.ts
│   │   └── __tests__/
│   │       └── GetUsersUseCase.test.ts
│   └── errors/
│       ├── AppError.ts                   # Base error class
│       ├── NetworkError.ts
│       ├── AuthError.ts
│       └── ValidationError.ts
│
├── data/
│   ├── api/
│   │   └── UserAPI.ts
│   ├── dtos/
│   │   └── UserDTO.ts
│   ├── mappers/
│   │   └── UserMapper.ts
│   ├── repositories/
│   │   ├── UserRepositoryImpl.ts
│   │   └── __tests__/
│   │       └── UserRepositoryImpl.test.ts
│   └── datasources/
│       ├── remote/
│       │   └── UserRemoteDataSource.ts
│       ├── cache/
│       │   └── UserCacheDataSource.ts
│       └── local/
│           └── UserLocalDataSource.ts
│
├── infrastructure/
│   ├── http/
│   │   ├── httpClient.ts                 # Axios/Ky instance with base config
│   │   ├── interceptors.ts               # Auth token injection, refresh, error mapping
│   │   └── retryPolicy.ts               # Exponential backoff, circuit breaker
│   ├── storage/
│   │   ├── MMKVStorage.ts                # Key-value store (MMKV)
│   │   └── SecureStorage.ts             # Keychain/Keystore for tokens & secrets
│   ├── analytics/
│   │   ├── AnalyticsService.ts
│   │   └── AnalyticsProvider.ts         # Abstraction over Firebase/Mixpanel/etc.
│   ├── logging/
│   │   └── Logger.ts                     # Structured logging with log levels
│   ├── crashlytics/
│   │   └── CrashlyticsService.ts
│   ├── push/
│   │   ├── PushNotificationService.ts
│   │   └── NotificationHandler.ts
│   ├── realtime/
│   │   ├── WebSocketClient.ts
│   │   └── SocketEventBus.ts            # Event-driven messaging for real-time features
│   ├── background/
│   │   └── BackgroundTaskManager.ts     # Background fetch, task scheduling
│   ├── permissions/
│   │   └── PermissionManager.ts         # Unified permission request/check API
│   ├── native/
│   │   └── NativeModuleBridge.ts        # Typed wrappers around native modules
│   ├── codepush/
│   │   └── CodePushService.ts           # OTA update management
│   └── di/
│       └── container.ts                  # DI container (tsyringe, inversify, or manual)
│
├── shared/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Modal.tsx
│   │   ├── FormInput.tsx
│   │   ├── ErrorFallback.tsx            # Error boundary fallback UI
│   │   ├── Skeleton.tsx                 # Loading skeleton
│   │   └── EmptyState.tsx
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   ├── useNetworkStatus.ts          # Online/offline detection
│   │   ├── useAppState.ts              # Foreground/background detection
│   │   └── usePermission.ts
│   ├── utils/
│   │   ├── formatDate.ts
│   │   ├── formatCurrency.ts
│   │   ├── validation.ts               # Zod/Yup schema helpers
│   │   └── platform.ts                 # Platform-specific utilities
│   ├── constants/
│   │   └── Colors.ts
│   ├── types/
│   │   ├── navigation.d.ts             # Typed navigation params
│   │   └── index.d.ts
│   ├── i18n/
│   │   ├── i18n.ts                      # i18next setup
│   │   ├── en.json
│   │   └── ar.json
│   └── theme/
│       ├── tokens.ts                    # Design tokens (spacing, radii, typography scales)
│       ├── typography.ts
│       └── shadows.ts
│
├── e2e/
│   ├── auth.e2e.ts                       # Detox / Maestro E2E tests
│   └── helpers/
│       └── testUtils.ts
│
└── App.tsx

Layer Explanation

app/ — Composition Root

Entry point and wiring layer. Owns the global provider tree, root navigation, environment config, feature flags, and the bootstrap sequence. This is the only layer that "knows about everything" — it composes features/, domain/, data/, and infrastructure/ together.

features/ — Feature Modules

Vertical slices of the application, each containing its own screens, components, hooks, viewmodels, and navigation. Features communicate only through domain-level contracts (use cases, entities, events) — never by importing from each other directly. Each feature exposes a public API via index.ts barrel exports.

domain/ — Business Logic Core

The innermost layer. Contains entities (plain objects/classes, no framework dependencies), repository interfaces, service interfaces, use cases, and domain-specific error types. This layer has zero external dependencies — no React, no Axios, no storage libraries. It is pure TypeScript and trivially unit-testable.

data/ — Data Access

Implements domain repository interfaces. Owns DTOs (API response shapes), mappers (DTO-to-entity transformation), and data sources (remote, local, cache). Repository implementations here orchestrate between data sources and apply caching/sync strategies.

infrastructure/ — Platform & Third-Party Adapters

Wraps all third-party SDKs and platform APIs behind stable interfaces: HTTP clients, secure storage, analytics, crash reporting, push notifications, WebSockets, background tasks, permissions, native modules, and OTA updates. Swapping a vendor (e.g., Firebase to Mixpanel) means changing only this layer.

shared/ — Cross-Cutting Concerns

Reusable UI components, hooks, utilities, constants, types, internationalization, and design tokens. Anything that two or more features need but that doesn't contain business logic belongs here.


Key Architectural Practices

1. Dependency Injection

All cross-layer dependencies are injected via container.ts. Features receive use cases, use cases receive repository interfaces, repository implementations receive data sources. This enables easy mocking and testing.

2. DTO-to-Entity Mapping

API responses are never used directly in the domain or UI. UserDTO (API shape) is mapped to User (domain entity) via UserMapper. This decouples the API contract from the internal model — API changes don't cascade through the app.

3. Use Case Layer

Each business operation is a single-responsibility use case class (e.g., GetUsersUseCase, LoginUseCase). Use cases orchestrate repositories and domain services, keeping business rules out of ViewModels and hooks.

4. Repository Pattern

domain/repositories/ defines interfaces. data/repositories/ provides implementations. This lets you swap from REST to GraphQL, or add an offline-first caching layer, without touching business logic.

5. ViewModel / Hook as Controller

ViewModels encapsulate presentation logic (loading states, error mapping, data transformation for UI). Hooks (useLogin, useUsers) connect ViewModels to React lifecycle. Screens remain thin — they render UI and delegate to hooks.

6. Feature Module Boundaries

Features are self-contained. Cross-feature communication happens through:

  • Domain events (event bus pattern) for loose coupling
  • Shared navigation params for screen-to-screen transitions
  • Domain entities/use cases as the shared contract

Features never import from each other's internal folders.

7. Offline-First Strategy

Remote DataSource → Cache DataSource → Local DataSource
         ↓                  ↓                  ↓
    Network API         In-memory/MMKV     SQLite/WatermelonDB
  • Read path: Check cache first, fall back to remote, persist to local.
  • Write path: Write to local immediately (optimistic), sync to remote, reconcile on conflict.
  • Sync engine: Background task manager triggers periodic sync when connectivity resumes.

8. Error Handling Strategy

  • Domain errors (AppError subclasses) represent typed, recoverable failures.
  • Infrastructure interceptors catch HTTP/network errors and map them to domain error types.
  • React Error Boundaries (via ErrorBoundaryProvider) catch rendering crashes with a fallback UI.
  • Global error handler in bootstrap.tsx catches unhandled JS exceptions and promise rejections, logs them, and reports to Crashlytics.

9. State Management

Concern Tool Location
Server/async state React Query (TanStack Query) Feature hooks
Client/UI state Zustand app/store/ + feature-local stores
Form state React Hook Form + Zod Feature components
Navigation state React Navigation app/navigation/
Persisted state MMKV + Zustand persist middleware infrastructure/storage/

10. Performance Optimizations

  • React Query for automatic caching, deduplication, background refetching, and stale-while-revalidate.
  • FlashList over FlatList for large lists (60fps scrolling at scale).
  • React.memo / useMemo / useCallback applied judiciously — profile first, memoize second.
  • Hermes engine enabled for faster startup and lower memory.
  • Lazy loading of feature modules via React.lazy + Suspense where applicable.
  • Image optimization with react-native-fast-image and progressive loading.
  • Bundle analysis and tree-shaking to minimize JS bundle size.
  • Reanimated 3 for 60fps animations on the UI thread.

11. Security

  • Tokens stored in Keychain/Keystore via SecureStorage, never in AsyncStorage or MMKV.
  • Certificate pinning configured in the HTTP client for production builds.
  • Biometric authentication gating for sensitive operations.
  • ProGuard/R8 (Android) and bitcode (iOS) for code obfuscation.
  • Runtime jailbreak/root detection with appropriate degradation.
  • No sensitive data in Redux/Zustand devtools in production.

12. Testing Strategy

Layer Test Type Tools What to Test
domain/usecases/ Unit Jest Business logic in isolation, mocked repos
data/repositories/ Unit Jest Data orchestration, mapper correctness
features/viewmodels/ Unit Jest Presentation logic, state transitions
features/screens/ Component RNTL Render output, user interactions
infrastructure/ Integration Jest HTTP interceptors, storage adapters
e2e/ E2E Detox / Maestro Critical user journeys

Colocate tests with source (__tests__/ folders) for discoverability. Aim for high coverage on domain/ and data/, pragmatic coverage on features/.

13. Navigation Architecture

  • RootNavigator owns the top-level auth/unauth split and modal stack.
  • Each feature owns its own navigator (stack/tab) registered into the root.
  • Deep linking configured via linking.ts with universal links (iOS) and app links (Android).
  • Typed navigation using declaration merging on RootParamList for compile-time safety.

14. Internationalization (i18n)

  • i18next + react-i18next for runtime translations.
  • Translation files colocated in shared/i18n/ with per-locale JSON files.
  • RTL layout support for Arabic and other RTL languages.
  • Dynamic locale switching without app restart.

15. Observability

  • Structured logging with severity levels (debug, info, warn, error) routed to console in dev and to a remote logging service in production.
  • Analytics abstracted behind AnalyticsProvider so the feature layer tracks events without knowing the vendor.
  • Crashlytics for crash reporting with breadcrumbs from the logger.
  • Performance monitoring (startup time, screen render time, API latency) via Firebase Performance or custom instrumentation.

16. CI/CD Considerations

  • Fastlane for automated iOS/Android builds and store submissions.
  • CodePush / EAS Update for OTA JS bundle updates without store review.
  • Per-environment builds driven by app/config/env.ts (dev, staging, production).
  • Pre-commit hooks (Husky + lint-staged) for linting and formatting.
  • PR-level checks: TypeScript compilation, ESLint, Jest, Detox smoke tests.

17. Scalability Patterns for Multi-Team Development

  • Feature ownership: Each team owns one or more features/ modules with clear barrel-export boundaries.
  • Dependency rule enforcement: Use ESLint plugin (eslint-plugin-boundaries or dependency-cruiser) to prevent illegal cross-layer or cross-feature imports.
  • Shared component library: shared/components/ follows atomic design principles; changes go through design system review.
  • API contract testing: Pact or similar contract tests between mobile and backend teams.
  • Feature flags: featureFlags.ts integrates with a remote config service (LaunchDarkly, Firebase Remote Config) for safe rollouts and A/B testing.

Example: Data Flow for "Get Users"

UserListScreen
  └── useUsers() hook
        └── UserListViewModel
              └── GetUsersUseCase.execute()
                    └── UserRepository.getUsers()          ← domain interface
                          └── UserRepositoryImpl.getUsers() ← data implementation
                                ├── UserCacheDataSource.get()
                                │     └── (cache hit → return mapped entities)
                                └── UserRemoteDataSource.fetch()
                                      └── UserAPI.getUsers()
                                            └── httpClient.get('/users')
                                                  └── interceptors (auth token, retry, error mapping)
                                      └── UserMapper.toDomain(dto)
                                      └── UserCacheDataSource.set(entities)

Example: Domain Entity

// domain/entities/User.ts
export interface User {
  readonly id: string;
  readonly email: string;
  readonly displayName: string;
  readonly avatarUrl: string | null;
  readonly role: UserRole;
  readonly createdAt: Date;
}

export type UserRole = 'admin' | 'member' | 'guest';

Example: Use Case

// domain/usecases/GetUsersUseCase.ts
import type { User } from '../entities/User';
import type { UserRepository } from '../repositories/UserRepository';

export class GetUsersUseCase {
  constructor(private readonly userRepository: UserRepository) {}

  async execute(page: number, limit: number): Promise<User[]> {
    return this.userRepository.getUsers(page, limit);
  }
}

Example: Repository Interface vs Implementation

// domain/repositories/UserRepository.ts — interface in domain layer
import type { User } from '../entities/User';

export interface UserRepository {
  getUsers(page: number, limit: number): Promise<User[]>;
  getUserById(id: string): Promise<User>;
}
// data/repositories/UserRepositoryImpl.ts — implementation in data layer
import type { UserRepository } from '../../domain/repositories/UserRepository';
import type { User } from '../../domain/entities/User';
import type { UserRemoteDataSource } from '../datasources/remote/UserRemoteDataSource';
import type { UserCacheDataSource } from '../datasources/cache/UserCacheDataSource';
import { UserMapper } from '../mappers/UserMapper';

export class UserRepositoryImpl implements UserRepository {
  constructor(
    private readonly remote: UserRemoteDataSource,
    private readonly cache: UserCacheDataSource,
  ) {}

  async getUsers(page: number, limit: number): Promise<User[]> {
    const cached = await this.cache.getUsers(page);
    if (cached) return cached;

    const dtos = await this.remote.fetchUsers(page, limit);
    const users = dtos.map(UserMapper.toDomain);
    await this.cache.setUsers(page, users);
    return users;
  }

  async getUserById(id: string): Promise<User> {
    const dto = await this.remote.fetchUserById(id);
    return UserMapper.toDomain(dto);
  }
}

Example: ViewModel

// features/users/viewmodels/UserListViewModel.ts
import type { User } from '../../../domain/entities/User';
import type { GetUsersUseCase } from '../../../domain/usecases/GetUsersUseCase';

export class UserListViewModel {
  constructor(private readonly getUsersUseCase: GetUsersUseCase) {}

  async loadUsers(page: number): Promise<{
    users: User[];
    error: string | null;
  }> {
    try {
      const users = await this.getUsersUseCase.execute(page, 20);
      return { users, error: null };
    } catch (e) {
      return { users: [], error: this.mapError(e) };
    }
  }

  private mapError(error: unknown): string {
    if (error instanceof NetworkError) return 'No internet connection.';
    if (error instanceof AuthError) return 'Session expired. Please log in again.';
    return 'Something went wrong. Please try again.';
  }
}

Example: Feature Hook (Controller)

// features/users/hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { container } from '../../../infrastructure/di/container';

export function useUsers(page: number) {
  const viewModel = container.resolve(UserListViewModel);

  return useQuery({
    queryKey: ['users', page],
    queryFn: () => viewModel.loadUsers(page),
    staleTime: 5 * 60 * 1000,
  });
}

Example: DI Container

// infrastructure/di/container.ts
import { UserAPI } from '../../data/api/UserAPI';
import { UserRemoteDataSource } from '../../data/datasources/remote/UserRemoteDataSource';
import { UserCacheDataSource } from '../../data/datasources/cache/UserCacheDataSource';
import { UserRepositoryImpl } from '../../data/repositories/UserRepositoryImpl';
import { GetUsersUseCase } from '../../domain/usecases/GetUsersUseCase';
import { httpClient } from '../http/httpClient';

const userApi = new UserAPI(httpClient);
const userRemote = new UserRemoteDataSource(userApi);
const userCache = new UserCacheDataSource();
const userRepository = new UserRepositoryImpl(userRemote, userCache);

export const container = {
  getUsersUseCase: () => new GetUsersUseCase(userRepository),
  // Register additional use cases, services, etc.
};

Recommended Library Stack

Concern Library
Navigation React Navigation 7+
Server state TanStack Query (React Query) v5
Client state Zustand
Forms React Hook Form + Zod
HTTP Axios or Ky
Storage (KV) react-native-mmkv
Secure storage react-native-keychain
Lists @shopify/flash-list
Animations react-native-reanimated 3
i18n i18next + react-i18next
Testing Jest + React Native Testing Library
E2E Detox or Maestro
OTA updates CodePush / EAS Update
Crash reporting Firebase Crashlytics / Sentry
Analytics Segment / Firebase Analytics
Image loading react-native-fast-image
Styling Nativewind (Tailwind) or StyleSheet

This architecture supports multi-team development, large feature sets, offline-first capabilities, full testability, and maintainable scaling over years — while keeping each layer independently replaceable and every boundary enforceable via tooling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment