Skip to content

React Native Setup and Configuration

Overview

React Native setup with modern development patterns, TypeScript integration, and cross-platform mobile development, following AzmX development standards for scalable mobile applications.

Core Dependencies

From typical React Native package.json:

{
  "dependencies": {
    "react": "18.3.1",
    "react-native": "0.75.2",
    "@react-navigation/native": "^6.1.18",
    "@react-navigation/stack": "^6.4.1",
    "@react-navigation/bottom-tabs": "^6.6.1",
    "react-native-screens": "^3.34.0",
    "react-native-safe-area-context": "^4.10.8",
    "react-native-gesture-handler": "^2.18.1",
    "@react-native-async-storage/async-storage": "^1.24.0",
    "react-native-vector-icons": "^10.1.0",
    "react-native-svg": "^15.6.0"
  },
  "devDependencies": {
    "@types/react": "^18.3.5",
    "@types/react-native": "^0.73.0",
    "typescript": "^5.5.4",
    "@react-native/eslint-config": "^0.75.2",
    "@react-native/metro-config": "^0.75.2",
    "@react-native/typescript-config": "^0.75.2",
    "jest": "^29.7.0",
    "@testing-library/react-native": "^12.6.1"
  }
}

Project Structure

ReactNativeApp/
├── android/                     # Android native code
├── ios/                         # iOS native code
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── Button/
│   │   │   │   ├── Button.tsx
│   │   │   │   ├── Button.test.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Input/
│   │   │   ├── Modal/
│   │   │   └── LoadingSpinner/
│   │   └── screens/
│   │       ├── Home/
│   │       ├── Profile/
│   │       └── Settings/
│   ├── screens/
│   │   ├── AuthScreens/
│   │   │   ├── LoginScreen.tsx
│   │   │   └── SignupScreen.tsx
│   │   ├── MainScreens/
│   │   │   ├── HomeScreen.tsx
│   │   │   └── ProfileScreen.tsx
│   │   └── index.ts
│   ├── navigation/
│   │   ├── AppNavigator.tsx
│   │   ├── AuthNavigator.tsx
│   │   └── TabNavigator.tsx
│   ├── services/
│   │   ├── api.ts
│   │   ├── auth.ts
│   │   ├── storage.ts
│   │   └── permissions.ts
│   ├── hooks/
│   │   ├── useApi.ts
│   │   ├── useAuth.ts
│   │   └── useStorage.ts
│   ├── types/
│   │   ├── navigation.ts
│   │   ├── api.ts
│   │   └── user.ts
│   ├── utils/
│   │   ├── constants.ts
│   │   ├── helpers.ts
│   │   └── validation.ts
│   ├── styles/
│   │   ├── colors.ts
│   │   ├── typography.ts
│   │   └── spacing.ts
│   └── App.tsx
├── __tests__/
├── metro.config.js
├── tsconfig.json
├── babel.config.js
└── package.json

TypeScript Configuration

TSConfig (tsconfig.json)

{
  "extends": "@react-native/typescript-config/tsconfig.json",
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@/components/*": ["components/*"],
      "@/screens/*": ["screens/*"],
      "@/navigation/*": ["navigation/*"],
      "@/services/*": ["services/*"],
      "@/hooks/*": ["hooks/*"],
      "@/types/*": ["types/*"],
      "@/utils/*": ["utils/*"],
      "@/styles/*": ["styles/*"]
    }
  },
  "include": [
    "src/**/*",
    "__tests__/**/*"
  ],
  "exclude": [
    "node_modules",
    "android",
    "ios"
  ]
}

Metro Configuration (metro.config.js)

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config')

/**
 * Metro configuration
 * https://facebook.github.io/metro/docs/configuration
 */
const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  resolver: {
    alias: {
      '@': './src',
    },
  },
}

module.exports = mergeConfig(getDefaultConfig(__dirname), config)

Type Definitions

import { NavigatorScreenParams } from '@react-navigation/native'

export type RootStackParamList = {
  Auth: NavigatorScreenParams<AuthStackParamList>
  Main: NavigatorScreenParams<MainTabParamList>
  Modal: {
    title: string
    content: string
  }
}

export type AuthStackParamList = {
  Login: undefined
  Signup: undefined
  ForgotPassword: undefined
}

export type MainTabParamList = {
  Home: undefined
  Search: undefined
  Profile: {
    userId?: string
  }
  Settings: undefined
}

export type HomeStackParamList = {
  HomeScreen: undefined
  Details: {
    itemId: string
  }
}

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

API Types (src/types/api.ts)

// Base response structure
export interface ApiResponse<T = any> {
  success: boolean
  data?: T
  message?: string
  errors?: Record<string, string[]>
}

// Network request states
export type RequestState = 'idle' | 'loading' | 'success' | 'error'

export interface ApiError {
  message: string
  code?: string
  statusCode?: number
}

export interface PaginationParams {
  page: number
  limit: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}

export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    total: number
    page: number
    limit: number
    totalPages: number
  }
}

// Device and app info
export interface DeviceInfo {
  platform: 'ios' | 'android'
  version: string
  buildNumber: string
  deviceId: string
  appVersion: string
}

User Types (src/types/user.ts)

export interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  avatar?: string
  phoneNumber?: string
  dateOfBirth?: string
  role: UserRole
  isActive: boolean
  preferences: UserPreferences
  createdAt: string
  updatedAt: string
}

export type UserRole = 'user' | 'admin' | 'moderator'

export interface UserPreferences {
  theme: 'light' | 'dark' | 'system'
  language: string
  notifications: NotificationPreferences
}

export interface NotificationPreferences {
  push: boolean
  email: boolean
  sms: boolean
  marketing: boolean
}

export interface AuthTokens {
  accessToken: string
  refreshToken: string
  expiresAt: string
}

export interface LoginRequest {
  email: string
  password: string
  deviceInfo: DeviceInfo
}

export interface SignupRequest {
  email: string
  password: string
  firstName: string
  lastName: string
  phoneNumber?: string
  deviceInfo: DeviceInfo
}

Styling System

Colors (src/styles/colors.ts)

export const colors = {
  // Primary colors
  primary: {
    50: '#eff6ff',
    100: '#dbeafe',
    200: '#bfdbfe',
    300: '#93c5fd',
    400: '#60a5fa',
    500: '#3b82f6',
    600: '#2563eb',
    700: '#1d4ed8',
    800: '#1e40af',
    900: '#1e3a8a',
  },

  // Neutral colors
  gray: {
    50: '#f9fafb',
    100: '#f3f4f6',
    200: '#e5e7eb',
    300: '#d1d5db',
    400: '#9ca3af',
    500: '#6b7280',
    600: '#4b5563',
    700: '#374151',
    800: '#1f2937',
    900: '#111827',
  },

  // Semantic colors
  success: '#10b981',
  warning: '#f59e0b',
  error: '#ef4444',
  info: '#3b82f6',

  // Background colors
  background: {
    light: '#ffffff',
    dark: '#000000',
  },

  // Text colors
  text: {
    primary: '#111827',
    secondary: '#6b7280',
    light: '#ffffff',
  },
} as const

export type ColorKey = keyof typeof colors

Typography (src/styles/typography.ts)

import { TextStyle } from 'react-native'

export const typography = {
  // Font families
  fontFamily: {
    regular: 'Inter-Regular',
    medium: 'Inter-Medium',
    semiBold: 'Inter-SemiBold',
    bold: 'Inter-Bold',
  },

  // Font sizes
  fontSize: {
    xs: 12,
    sm: 14,
    base: 16,
    lg: 18,
    xl: 20,
    '2xl': 24,
    '3xl': 30,
    '4xl': 36,
  },

  // Line heights
  lineHeight: {
    tight: 1.25,
    snug: 1.375,
    normal: 1.5,
    relaxed: 1.625,
    loose: 2,
  },

  // Text styles
  styles: {
    h1: {
      fontSize: 30,
      fontFamily: 'Inter-Bold',
      lineHeight: 36,
    },
    h2: {
      fontSize: 24,
      fontFamily: 'Inter-SemiBold',
      lineHeight: 30,
    },
    h3: {
      fontSize: 20,
      fontFamily: 'Inter-SemiBold',
      lineHeight: 26,
    },
    body: {
      fontSize: 16,
      fontFamily: 'Inter-Regular',
      lineHeight: 24,
    },
    caption: {
      fontSize: 14,
      fontFamily: 'Inter-Regular',
      lineHeight: 20,
    },
    small: {
      fontSize: 12,
      fontFamily: 'Inter-Regular',
      lineHeight: 16,
    },
  } as Record<string, TextStyle>,
} as const

Component Architecture

Button Component (src/components/common/Button/Button.tsx)

import React from 'react'
import {
  TouchableOpacity,
  Text,
  ViewStyle,
  TextStyle,
  ActivityIndicator,
} from 'react-native'
import { colors, typography } from '@/styles'

export interface ButtonProps {
  title: string
  onPress: () => void
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
  style?: ViewStyle
  textStyle?: TextStyle
  testID?: string
}

export const Button: React.FC<ButtonProps> = ({
  title,
  onPress,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  loading = false,
  style,
  textStyle,
  testID,
}) => {
  const getButtonStyle = (): ViewStyle => {
    const baseStyle: ViewStyle = {
      borderRadius: 8,
      alignItems: 'center',
      justifyContent: 'center',
      flexDirection: 'row',
    }

    const sizeStyles = {
      small: { paddingHorizontal: 12, paddingVertical: 6 },
      medium: { paddingHorizontal: 16, paddingVertical: 10 },
      large: { paddingHorizontal: 20, paddingVertical: 14 },
    }

    const variantStyles = {
      primary: {
        backgroundColor: colors.primary[500],
        borderWidth: 0,
      },
      secondary: {
        backgroundColor: colors.gray[500],
        borderWidth: 0,
      },
      outline: {
        backgroundColor: 'transparent',
        borderWidth: 1,
        borderColor: colors.primary[500],
      },
      ghost: {
        backgroundColor: 'transparent',
        borderWidth: 0,
      },
    }

    return {
      ...baseStyle,
      ...sizeStyles[size],
      ...variantStyles[variant],
      ...(disabled && { opacity: 0.5 }),
      ...style,
    }
  }

  const getTextStyle = (): TextStyle => {
    const sizeStyles = {
      small: { fontSize: typography.fontSize.sm },
      medium: { fontSize: typography.fontSize.base },
      large: { fontSize: typography.fontSize.lg },
    }

    const variantStyles = {
      primary: { color: colors.text.light },
      secondary: { color: colors.text.light },
      outline: { color: colors.primary[500] },
      ghost: { color: colors.primary[500] },
    }

    return {
      fontFamily: typography.fontFamily.semiBold,
      ...sizeStyles[size],
      ...variantStyles[variant],
      ...textStyle,
    }
  }

  return (
    <TouchableOpacity
      style={getButtonStyle()}
      onPress={onPress}
      disabled={disabled || loading}
      activeOpacity={0.8}
      testID={testID}
    >
      {loading && (
        <ActivityIndicator
          size="small"
          color={variant === 'primary' ? colors.text.light : colors.primary[500]}
          style={{ marginRight: 8 }}
        />
      )}
      <Text style={getTextStyle()}>{title}</Text>
    </TouchableOpacity>
  )
}

export default Button

App Navigator (src/navigation/AppNavigator.tsx)

import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { useAuth } from '@/hooks/useAuth'
import { AuthNavigator } from './AuthNavigator'
import { TabNavigator } from './TabNavigator'
import { RootStackParamList } from '@/types/navigation'

const Stack = createStackNavigator<RootStackParamList>()

export const AppNavigator: React.FC = () => {
  const { isAuthenticated } = useAuth()

  return (
    <NavigationContainer>
      <Stack.Navigator
        screenOptions={{
          headerShown: false,
        }}
      >
        {isAuthenticated ? (
          <Stack.Screen name="Main" component={TabNavigator} />
        ) : (
          <Stack.Screen name="Auth" component={AuthNavigator} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  )
}

export default AppNavigator

Tab Navigator (src/navigation/TabNavigator.tsx)

import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import Icon from 'react-native-vector-icons/Feather'
import { colors } from '@/styles'
import { MainTabParamList } from '@/types/navigation'
import { HomeScreen } from '@/screens/MainScreens/HomeScreen'
import { ProfileScreen } from '@/screens/MainScreens/ProfileScreen'
import { SettingsScreen } from '@/screens/MainScreens/SettingsScreen'

const Tab = createBottomTabNavigator<MainTabParamList>()

export const TabNavigator: React.FC = () => {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarActiveTintColor: colors.primary[500],
        tabBarInactiveTintColor: colors.gray[400],
        tabBarLabelStyle: {
          fontSize: 12,
          fontFamily: 'Inter-Medium',
        },
        tabBarStyle: {
          backgroundColor: colors.background.light,
          borderTopColor: colors.gray[200],
        },
        headerShown: false,
      }}
    >
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="home" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Search"
        component={HomeScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="search" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Profile"
        component={ProfileScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="user" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Settings"
        component={SettingsScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="settings" size={size} color={color} />
          ),
        }}
      />
    </Tab.Navigator>
  )
}

API Service Layer

API Service (src/services/api.ts)

import AsyncStorage from '@react-native-async-storage/async-storage'
import { ApiResponse, ApiError } from '@/types/api'

class ApiService {
  private baseURL: string
  private timeout: number

  constructor() {
    this.baseURL = __DEV__
      ? 'http://localhost:8000/api'
      : 'https://api.azmx.sa/api'
    this.timeout = 10000
  }

  private async getAuthToken(): Promise<string | null> {
    try {
      return await AsyncStorage.getItem('accessToken')
    } catch (error) {
      console.error('Failed to get auth token:', error)
      return null
    }
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<ApiResponse<T>> {
    const token = await this.getAuthToken()

    const config: RequestInit = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    }

    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), this.timeout)

    try {
      const response = await fetch(`${this.baseURL}${endpoint}`, {
        ...config,
        signal: controller.signal,
      })

      clearTimeout(timeoutId)

      if (!response.ok) {
        throw new ApiError(
          `HTTP error! status: ${response.status}`,
          response.status.toString(),
          response.status
        )
      }

      const data = await response.json()
      return data
    } catch (error) {
      clearTimeout(timeoutId)

      if (error instanceof ApiError) {
        throw error
      }

      throw new ApiError(
        error instanceof Error ? error.message : 'Network error'
      )
    }
  }

  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, { method: 'GET' })
  }

  async post<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: data ? JSON.stringify(data) : undefined,
    })
  }

  async put<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: data ? JSON.stringify(data) : undefined,
    })
  }

  async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, { method: 'DELETE' })
  }
}

export class ApiError extends Error {
  constructor(
    message: string,
    public code?: string,
    public statusCode?: number
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

export const apiService = new ApiService()
export default apiService

Custom Hooks

useAuth Hook (src/hooks/useAuth.ts)

import { useState, useEffect, useCallback } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { User, AuthTokens, LoginRequest, SignupRequest } from '@/types/user'
import { apiService } from '@/services/api'

interface AuthState {
  user: User | null
  isAuthenticated: boolean
  isLoading: boolean
  error: string | null
}

export const useAuth = () => {
  const [authState, setAuthState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    isLoading: true,
    error: null,
  })

  // Initialize auth state
  useEffect(() => {
    initializeAuth()
  }, [])

  const initializeAuth = async () => {
    try {
      const token = await AsyncStorage.getItem('accessToken')
      const userString = await AsyncStorage.getItem('user')

      if (token && userString) {
        const user = JSON.parse(userString)
        setAuthState({
          user,
          isAuthenticated: true,
          isLoading: false,
          error: null,
        })
      } else {
        setAuthState(prev => ({
          ...prev,
          isLoading: false,
        }))
      }
    } catch (error) {
      setAuthState({
        user: null,
        isAuthenticated: false,
        isLoading: false,
        error: 'Failed to initialize authentication',
      })
    }
  }

  const login = useCallback(async (loginData: LoginRequest) => {
    try {
      setAuthState(prev => ({ ...prev, isLoading: true, error: null }))

      const response = await apiService.post<{
        user: User
        tokens: AuthTokens
      }>('/auth/login', loginData)

      if (response.success && response.data) {
        const { user, tokens } = response.data

        // Store tokens and user data
        await AsyncStorage.multiSet([
          ['accessToken', tokens.accessToken],
          ['refreshToken', tokens.refreshToken],
          ['user', JSON.stringify(user)],
        ])

        setAuthState({
          user,
          isAuthenticated: true,
          isLoading: false,
          error: null,
        })
      }
    } catch (error) {
      setAuthState(prev => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Login failed',
      }))
    }
  }, [])

  const signup = useCallback(async (signupData: SignupRequest) => {
    try {
      setAuthState(prev => ({ ...prev, isLoading: true, error: null }))

      const response = await apiService.post<{
        user: User
        tokens: AuthTokens
      }>('/auth/signup', signupData)

      if (response.success && response.data) {
        const { user, tokens } = response.data

        await AsyncStorage.multiSet([
          ['accessToken', tokens.accessToken],
          ['refreshToken', tokens.refreshToken],
          ['user', JSON.stringify(user)],
        ])

        setAuthState({
          user,
          isAuthenticated: true,
          isLoading: false,
          error: null,
        })
      }
    } catch (error) {
      setAuthState(prev => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Signup failed',
      }))
    }
  }, [])

  const logout = useCallback(async () => {
    try {
      await AsyncStorage.multiRemove(['accessToken', 'refreshToken', 'user'])

      setAuthState({
        user: null,
        isAuthenticated: false,
        isLoading: false,
        error: null,
      })
    } catch (error) {
      console.error('Logout error:', error)
    }
  }, [])

  return {
    ...authState,
    login,
    signup,
    logout,
  }
}

Testing Configuration

Jest Configuration (jest.config.js)

module.exports = {
  preset: 'react-native',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|react-navigation|@react-navigation|react-native-vector-icons)/)',
  ],
  setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/types/**/*',
  ],
}

Test Setup (tests/setup.ts)

import 'react-native-gesture-handler/jestSetup'

// Mock react-native-vector-icons
jest.mock('react-native-vector-icons/Feather', () => 'Icon')

// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
)

// Mock react-navigation
jest.mock('@react-navigation/native', () => {
  const actualNav = jest.requireActual('@react-navigation/native')
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      goBack: jest.fn(),
    }),
    useRoute: () => ({
      params: {},
    }),
  }
})

// Silence the warning: Animated: `useNativeDriver` is not supported
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')

Build Configuration

Android Build (android/app/build.gradle)

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        applicationId "com.azmx.app"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
    }

    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            // Signing config for release builds
            signingConfig signingConfigs.release
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

    splits {
        abi {
            reset()
            enable enableSeparateBuildPerCPUArchitecture
            universalApk false
            include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
        }
    }
}

iOS Build Configuration (ios/Podfile)

# Resolve react_native_pods.rb with node to allow for hoisting
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, '13.0'
prepare_react_native_project!

target 'AzmXApp' do
  config = use_native_modules!

  use_react_native!(
    :path => config[:reactNativePath],
    :hermes_enabled => true,
    :fabric_enabled => flags[:fabric_enabled],
    :flipper_configuration => FlipperConfiguration.enabled,
    :app_clip => false
  )

  target 'AzmXAppTests' do
    inherit! :complete
  end

  post_install do |installer|
    react_native_post_install(
      installer,
      config[:reactNativePath],
      :mac_catalyst_enabled => false
    )
    __apply_Xcode_12_5_M1_post_install_workaround(installer)
  end
end

Performance Optimizations

Image Optimization

import React from 'react'
import { Image, ImageProps } from 'react-native'
import FastImage, { FastImageProps } from 'react-native-fast-image'

interface OptimizedImageProps extends Omit<FastImageProps, 'source'> {
  source: string | { uri: string }
  fallback?: ImageProps['source']
}

export const OptimizedImage: React.FC<OptimizedImageProps> = ({
  source,
  fallback,
  ...props
}) => {
  const imageSource = typeof source === 'string' ? { uri: source } : source

  return (
    <FastImage
      source={imageSource}
      resizeMode={FastImage.resizeMode.cover}
      fallback={fallback}
      {...props}
    />
  )
}

Memory Management

import { useEffect, useRef } from 'react'
import { AppState, AppStateStatus } from 'react-native'

export const useAppState = (callback: (state: AppStateStatus) => void) => {
  const appStateRef = useRef(AppState.currentState)

  useEffect(() => {
    const handleAppStateChange = (nextAppState: AppStateStatus) => {
      appStateRef.current = nextAppState
      callback(nextAppState)
    }

    const subscription = AppState.addEventListener('change', handleAppStateChange)

    return () => subscription?.remove()
  }, [callback])

  return appStateRef.current
}

Development Commands

Package.json Scripts

{
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
    "type-check": "tsc --noEmit",
    "clean": "react-native clean",
    "pod-install": "cd ios && pod install",
    "build:android": "cd android && ./gradlew assembleRelease",
    "build:ios": "react-native run-ios --configuration Release"
  }
}

Common Development Tasks

# Start Metro bundler
npm start

# Run on Android
npm run android

# Run on iOS
npm run ios

# Install iOS dependencies
npm run pod-install

# Clean project
npm run clean

# Type checking
npm run type-check

# Run tests
npm test

# Build for Android
npm run build:android

# Build for iOS
npm run build:ios

References