Lesson 13-React Native Project Architecture and Engineering

Project Architecture Design

Single Page Application (SPA) vs. Multi-Page Application Architecture

SPA Architecture Implementation

// App.js - Single Page Application Entry
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';

const Stack = createStackNavigator();

const App = () => (
  <NavigationContainer>
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Detail" component={DetailScreen} />
    </Stack.Navigator>
  </NavigationContainer>
);

export default App;

Multi-Page Application Architecture Design

// Multi-Page Application Configuration Example (Achieved via Multiple Entry Files)
// app1/App.js
import React from 'react';
import { View, Text } from 'react-native';
const App1 = () => <View><Text>App 1</Text></View>;
export default App1;

// app2/App.js
import React from 'react';
import { View, Text } from 'react-native';
const App2 = () => <View><Text>App 2</Text></View>;
export default App2;

// Implement Multiple Entries via Different Build Configurations
// metro.config.js
module.exports = {
  resolver: {
    extraNodeModules: {
      app1: path.resolve(__dirname, 'app1'),
      app2: path.resolve(__dirname, 'app2'),
    },
  },
};

Modular and Component-Based Design Principles

Functional Module Division Example

src/
├── modules/
│   ├── auth/          # Authentication Module
│   │   ├── components/ # Module-Specific Components
│   │   ├── screens/    # Module-Specific Screens
│   │   ├── hooks/      # Module-Specific Custom Hooks
│   │   └── api.js      # Module API Requests
│   ├── product/       # Product Module
│   └── order/         # Order Module
├── shared/            # Shared Module
│   ├── components/    # Global Shared Components
│   ├── utils/         # Utility Functions
│   └── constants/     # Global Constants

Component Design Specifications

// High Cohesion, Low Coupling Component Example
// components/Button/index.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';

const Button = ({ title, onPress, style, textStyle }) => (
  <TouchableOpacity style={[styles.container, style]} onPress={onPress}>
    <Text style={[styles.text, textStyle]}>{title}</Text>
  </TouchableOpacity>
);

const styles = StyleSheet.create({
  container: {
    padding: 12,
    borderRadius: 8,
    backgroundColor: '#2196F3',
  },
  text: {
    color: 'white',
    fontSize: 16,
  },
});

export default Button;

State Management Architecture Design (Redux, MobX)

Redux Architecture Implementation

// store/configureStore.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../modules/authSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
  },
});

// modules/authSlice.js
import { createSlice } from '@reduxjs/toolkit';

const authSlice = createSlice({
  name: 'auth',
  initialState: { isLoggedIn: false },
  reducers: {
    login: (state) => { state.isLoggedIn = true; },
    logout: (state) => { state.isLoggedIn = false; },
  },
});

export const { login, logout } = authSlice.actions;
export default authSlice.reducer;

MobX Architecture Implementation

// stores/AuthStore.js
import { makeAutoObservable } from 'mobx';

class AuthStore {
  isLoggedIn = false;

  constructor() {
    makeAutoObservable(this);
  }

  login() {
    this.isLoggedIn = true;
  }

  logout() {
    this.isLoggedIn = false;
  }
}

export default new AuthStore();

Routing Architecture Design (React Navigation)

Nested Routing Configuration

// navigation/AppNavigator.js
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import HomeStack from './HomeStack';
import ProfileStack from './ProfileStack';
import SettingsScreen from '../screens/SettingsScreen';

const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();

const HomeTabs = () => (
  <Tab.Navigator>
    <Tab.Screen name="Home" component={HomeStack} />
    <Tab.Screen name="Profile" component={ProfileStack} />
  </Tab.Navigator>
);

const AppNavigator = () => (
  <Stack.Navigator>
    <Stack.Screen name="Main" component={HomeTabs} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </Stack.Navigator>
);

export default AppNavigator;

Dynamic Routing Configuration

// Dynamic Routing Example (Based on User Permissions)
const getRoutesForUser = (userRole) => {
  const commonRoutes = [
    { name: 'Home', component: HomeScreen },
    { name: 'Profile', component: ProfileScreen },
  ];

  if (userRole === 'admin') {
    return [...commonRoutes, { name: 'Admin', component: AdminScreen }];
  }

  return commonRoutes;
};

const DynamicNavigator = ({ userRole }) => {
  const routes = getRoutesForUser(userRole);
  
  return (
    <Stack.Navigator>
      {routes.map(route => (
        <Stack.Screen 
          key={route.name}
          name={route.name}
          component={route.component}
        />
      ))}
    </Stack.Navigator>
  );
};

Project Directory Structure and Standards

my-app/
├── android/                  # Android Native Code
├── ios/                      # iOS Native Code
├── src/
│   ├── assets/               # Static Assets (Images, Fonts, etc.)
│   ├── components/           # Global Shared Components
│   ├── modules/              # Functional Modules
│   │   ├── auth/             # Authentication Module
│   │   ├── product/          # Product Module
│   │   └── order/            # Order Module
│   ├── navigation/           # Routing Configuration
│   ├── screens/              # Screen Components
│   ├── store/                # State Management
│   ├── styles/               # Global Styles
│   ├── utils/                # Utility Functions
│   ├── App.js                # Application Entry
│   └── index.js              # Render Entry
├── .eslintrc.js              # ESLint Configuration
├── .prettierrc               # Prettier Configuration
├── babel.config.js           # Babel Configuration
├── metro.config.js           # Metro Configuration
└── package.json

File Naming Conventions

1. Component Files:
   - PascalCase naming (e.g., `Button.js`)
   - Directory structure: `components/Button/index.js + components/Button/Button.styles.js`

2. Screen Files:
   - PascalCase naming (e.g., `HomeScreen.js`)
   - Directory structure: `screens/HomeScreen/index.js`

3. Utility Functions:
   - camelCase naming (e.g., `formatDate.js`)
   - Directory structure: `utils/formatDate.js`

4. Style Files:
   - Same directory as component, named `ComponentName.styles.js`

Engineering Toolchain

Advanced Configuration for React Native CLI

Custom Environment Variables

// .env File
API_URL=https://api.example.com
APP_VERSION=1.0.0

// Access Environment Variables Using react-native-config
import Config from 'react-native-config';

console.log(Config.API_URL); // Output: https://api.example.com

Custom Build Scripts

// package.json
{
  "scripts": {
    "start": "react-native start",
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "build:android": "cd android && ./gradlew assembleRelease",
    "build:ios": "cd ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release",
    "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
    "test": "jest"
  }
}

Deep Configuration for Metro Bundler

Cache Optimization Configuration

// metro.config.js
module.exports = {
  cacheStores: [
    new MetroCache.FileStore({
      root: path.join(__dirname, 'node_modules', '.cache', 'metro-cache'),
    }),
  ],
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true, // Enable inline requires to optimize startup performance
      },
    }),
  },
  maxWorkers: 4, // Adjust based on CPU cores
};

Custom Bundling Configuration

// metro.config.js
const { getDefaultConfig } = require('metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig();
  
  return {
    resolver: {
      sourceExts: [...sourceExts, 'jsx', 'tsx'],
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      extraNodeModules: {
        // Custom module resolution
      },
    },
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
  };
})();

TypeScript Integration and Configuration

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "lib": ["dom", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-native",
    "baseUrl": ".",
    "paths": {
      "@components/*": ["src/components/*"],
      "@screens/*": ["src/screens/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

Type Definition Example

// types/api.d.ts
declare namespace API {
  interface User {
    id: string;
    name: string;
    email: string;
  }

  type ApiResponse<T> = {
    data: T;
    error?: string;
  };
}

// Usage Example
import { API } from '../types/api';

const fetchUser = async (): Promise<API.ApiResponse<API.User>> => {
  // API call implementation
};

Code Standards and Style Guidelines

ESLint Configuration

// .eslintrc.js
module.exports = {
  root: true,
  extends: [
    '@react-native-community',
    'plugin:prettier/recommended',
  ],
  rules: {
    'react-native/no-inline-styles': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
  },
  settings: {
    'import/resolver': {
      node: {
        paths: ['src'],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
};

Prettier Configuration

// .prettierrc
{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": true,
  "jsxBracketSameLine": false,
  "arrowParens": "avoid"
}

Automated Testing Framework

Jest Unit Testing Example

// __tests__/Button.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Button from '../components/Button';

describe('Button Component', () => {
  it('renders correctly', () => {
    const { getByText } = render(<Button title="Test" />);
    expect(getByText('Test')).toBeTruthy();
  });

  it('calls onPress when pressed', () => {
    const onPressMock = jest.fn();
    const { getByText } = render(
      <Button title="Test" onPress={onPressMock} />
    );
    fireEvent.press(getByText('Test'));
    expect(onPressMock).toHaveBeenCalled();
  });
});

Detox End-to-End Testing Example

// e2e/login.spec.js
describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should login successfully', async () => {
    await element(by.id('emailInput')).typeText('user@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();
    
    await expect(element(by.text('Welcome'))).toBeVisible();
  });
});

Performance Optimization and Deployment

Code Splitting and Lazy Loading

Dynamic Import Implementation

// Use React.lazy for Component Lazy Loading
import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./components/HeavyComponent'));

const App = () => (
  <Suspense fallback={<ActivityIndicator />}>
    <LazyComponent />
  </Suspense>
);

// Route-Level Code Splitting
const HomeScreen = React.lazy(() => import('../screens/HomeScreen'));
const ProfileScreen = React.lazy(() => import('../screens/ProfileScreen'));

const AppNavigator = () => (
  <Stack.Navigator>
    <Stack.Screen name="Home">
      {props => (
        <Suspense fallback={<ActivityIndicator />}>
          <HomeScreen {...props} />
        </Suspense>
      )}
    </Stack.Screen>
    <Stack.Screen name="Profile">
      {props => (
        <Suspense fallback={<ActivityIndicator />}>
          <ProfileScreen {...props} />
        </Suspense>
      )}
    </Stack.Screen>
  </Stack.Navigator>
);

Resource Compression and Caching Strategies

Image Optimization Configuration

// Use react-native-fast-image for Optimized Image Loading
import FastImage from 'react-native-fast-image';

const OptimizedImage = () => (
  <FastImage
    style={{ width: 200, height: 200 }}
    source={{
      uri: 'https://example.com/image.jpg',
      priority: FastImage.priority.high,
      cache: FastImage.cacheControl.immutable,
    }}
    resizeMode={FastImage.resizeMode.contain}
  />
);

// Android Resource Compression (Gradle Configuration)
// android/app/build.gradle
android {
  aaptOptions {
    cruncherEnabled = false // Disable PNG compression (use WebP instead)
  }
}

Caching Strategy Implementation

// Network Request Caching Strategy
import { CacheManager } from 'react-native-expo-image-cache';

const getImage = async (url) => {
  const path = await CacheManager.get(url).getPath();
  if (path) {
    return path; // Return cached path
  }
  
  // Download and cache image
  await CacheManager.downloadFile({
    fromUrl: url,
    toFile: `${CacheManager.getCacheDirectory()}${md5(url)}`,
    cache: true,
    background: true,
  });
  
  return await CacheManager.get(url).getPath();
};

Application Packaging and Release

Android APK Packaging

# Generate Signing Key (if not already created)
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

# Configure Gradle Signing (gradle.properties)
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****

# Execute Build Command
cd android
./gradlew assembleRelease

iOS IPA Packaging

# Package Using Xcode
1. Open ios/MyApp.xcworkspace
2. Select Product > Archive
3. In Organizer window, select Archive and click Distribute App
4. Choose distribution method (App Store/Ad Hoc/Development)

# Command Line Packaging (requires certificate configuration)
xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -archivePath build/MyApp.xcarchive archive
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive -exportOptionsPlist ios/exportOptions.plist -exportPath build

Performance Monitoring and Analysis Tools

Flipper Integration

// Install Flipper Plugin
yarn add react-native-flipper

// Initialize Flipper (react-native.config.js)
module.exports = {
  plugins: ['react-native-flipper'],
};

// Use Performance Monitoring Plugin
import PerfMonitor from 'react-native-performance-monitor';

PerfMonitor.start();
// ...application code
PerfMonitor.stop();

Sentry Error Monitoring

// Install Sentry
yarn add @sentry/react-native

// Initialize Sentry
import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: 'https://your-dsn@sentry.io/project-id',
  enableNative: true,
  debug: __DEV__,
});

// Manually Capture Errors
try {
  // Code that might throw an error
} catch (error) {
  Sentry.captureException(error);
}

// Log Messages
Sentry.captureMessage('Something went wrong');

Hot Updates and OTA Updates

CodePush Configuration

// Install CodePush
yarn add react-native-code-push

// Link Native Dependencies
react-native link react-native-code-push

// Initialize CodePush (App.js)
import codePush from 'react-native-code-push';

const App = () => {
  // ...application code
};

export default codePush({
  checkFrequency: codePush.CheckFrequency.ON_APP_START,
  installMode: codePush.InstallMode.ON_NEXT_RESTART,
})(App);

// Release Update (Command Line)
code-push release-react MyApp android
code-push release-react MyApp ios

Custom OTA Update Solution

// Implement Custom Update Check Logic
const checkForUpdates = async () => {
  try {
    const response = await fetch('https://your-api.com/updates/latest');
    const { version, url } = await response.json();
    
    const currentVersion = DeviceInfo.getVersion();
    if (compareVersions(version, currentVersion) > 0) {
      // Prompt user to update
      Alert.alert(
        'New Version Available',
        'Download and install the new version now?',
        [
          { text: 'Later', style: 'cancel' },
          { text: 'Update', onPress: () => downloadUpdate(url) },
        ]
      );
    }
  } catch (error) {
    console.error('Failed to check for updates:', error);
  }
};

const downloadUpdate = async (url) => {
  // Implement download and install logic
  // Android: Use react-native-fs to download APK and call Intent to install
  // iOS: Requires enterprise certificate or TestFlight distribution
};

Share your love