← Back to Articles

Cross-Platform Mobile Development with React Native

Code
12 min read

React Native allows you to build native mobile applications using JavaScript and React. Instead of writing separate code for iOS and Android, you can write one codebase that works on both platforms. In this comprehensive guide, we'll explore React Native development from setup to deployment, covering everything you need to build production-ready mobile applications.

Why Choose React Native?

React Native offers several compelling advantages over traditional native development:

Cross-Platform Compatibility:

  • Single Codebase: Write once, deploy to iOS and Android
  • Native Performance: Compiles to native platform code
  • Hot Reloading: See changes instantly during development
  • Large Ecosystem: Extensive library support

Comparison with Other Solutions:

FrameworkLanguagePerformanceDevelopment SpeedLearning Curve
React NativeJavaScriptNativeFastModerate
FlutterDartNativeFastSteep
IonicJavaScriptWebViewFastEasy
Native iOSSwiftNativeSlowSteep
Native AndroidKotlinNativeSlowSteep

Development Environment Setup

Prerequisites:

  • Node.js (v14 or later)
  • npm or yarn
  • Xcode (for iOS development on macOS)
  • Android Studio (for Android development)

Installation:

# Install React Native CLI
npm install -g @react-native-community/cli

# Create new project
npx react-native init MyApp

# Or use Expo CLI for easier setup
npm install -g @expo/cli
expo init MyApp

Project Structure:

MyApp/
├── android/          # Android native code
├── ios/             # iOS native code
├── src/
│   ├── components/  # Reusable components
│   ├── screens/     # Screen components
│   ├── navigation/  # Navigation setup
│   └── utils/       # Helper functions
├── App.js           # Main app component
├── index.js         # Entry point
└── package.json

Core Components and APIs

1. Basic Components

View Component:

import React from 'react';
import { View, StyleSheet } from 'react-native';

const MyComponent = () => {
  return (
    <View style={styles.container}>
      <View style={styles.box} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#007AFF',
    borderRadius: 8,
  },
});

export default MyComponent;

Text and Input Components:

import React, { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';

const UserInput = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Name:</Text>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={setName}
        placeholder="Enter your name"
        autoCapitalize="words"
      />

      <Text style={styles.label}>Email:</Text>
      <TextInput
        style={styles.input}
        value={email}
        onChangeText={setEmail}
        placeholder="Enter your email"
        keyboardType="email-address"
        autoComplete="email"
      />

      <Text style={styles.displayText}>
        Hello, {name || 'Stranger'}!
        {email ? `
Your email: ${email}` : ''}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 20,
  },
  label: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 8,
    color: '#333',
  },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    marginBottom: 16,
    backgroundColor: '#fff',
  },
  displayText: {
    fontSize: 18,
    color: '#007AFF',
    marginTop: 20,
  },
});

2. Layout and Styling

Flexbox Layout:

import React from 'react';
import { View, StyleSheet } from 'react-native';

const LayoutExample = () => {
  return (
    <View style={styles.container}>
      {/* Header */}
      <View style={styles.header}>
        <Text style={styles.headerText}>Header</Text>
      </View>

      {/* Content */}
      <View style={styles.content}>
        <View style={styles.sidebar}>
          <Text>Sidebar</Text>
        </View>
        <View style={styles.main}>
          <Text>Main Content</Text>
        </View>
      </View>

      {/* Footer */}
      <View style={styles.footer}>
        <Text style={styles.footerText}>Footer</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    height: 60,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerText: {
    color: 'white',
    fontSize: 18,
    fontWeight: 'bold',
  },
  content: {
    flex: 1,
    flexDirection: 'row',
  },
  sidebar: {
    width: 100,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  main: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    alignItems: 'center',
  },
  footer: {
    height: 50,
    backgroundColor: '#333',
    justifyContent: 'center',
    alignItems: 'center',
  },
  footerText: {
    color: 'white',
    fontSize: 16,
  },
});

3. State Management

Local State with Hooks:

import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

const Counter = () => {
  const [count, setCount] = useState(0);
  const [isEven, setIsEven] = useState(true);

  useEffect(() => {
    setIsEven(count % 2 === 0);
  }, [count]);

  return (
    <View style={styles.container}>
      <Text style={styles.countText}>Count: {count}</Text>
      <Text style={[styles.evenText, { color: isEven ? 'green' : 'red' }]}>
        {isEven ? 'Even' : 'Odd'}
      </Text>

      <View style={styles.buttonContainer}>
        <Button
          title="Increment"
          onPress={() => setCount(count + 1)}
          color="#007AFF"
        />
        <Button
          title="Decrement"
          onPress={() => setCount(count - 1)}
          color="#FF3B30"
        />
        <Button
          title="Reset"
          onPress={() => setCount(0)}
          color="#34C759"
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  countText: {
    fontSize: 48,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  evenText: {
    fontSize: 24,
    marginBottom: 40,
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    width: '100%',
  },
});

Global State with Context:

// AppContext.js
import React, { createContext, useContext, useReducer } from 'react';

const AppContext = createContext();

const initialState = {
  user: null,
  theme: 'light',
  notifications: [],
};

function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload]
      };
    default:
      return state;
  }
}

export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
}

export function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
}

// Usage in components
import { useApp } from './AppContext';

const ThemeToggle = () => {
  const { state, dispatch } = useApp();

  return (
    <Button
      title={`Switch to ${state.theme === 'light' ? 'Dark' : 'Light'} Mode`}
      onPress={() => dispatch({ type: 'TOGGLE_THEME' })}
    />
  );
};

React Navigation Setup:

npm install @react-navigation/native
npm install @react-navigation/stack
npm install @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context

Stack Navigation:

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

import HomeScreen from './screens/HomeScreen';
import DetailsScreen from './screens/DetailsScreen';

const Stack = createStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerStyle: {
            backgroundColor: '#007AFF',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'Home' }}
        />
        <Stack.Screen
          name="Details"
          component={DetailsScreen}
          options={{ title: 'Details' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Tab Navigation:

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';
import SettingsScreen from './screens/SettingsScreen';

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Profile') {
              iconName = focused ? 'person' : 'person-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'settings' : 'settings-outline';
            }

            // Return appropriate icon component
            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: '#007AFF',
          tabBarInactiveTintColor: 'gray',
        })}
      >
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Profile" component={ProfileScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Native Modules and APIs

1. Device Features

Camera Access:

import React, { useState } from 'react';
import { View, Button, Image, Alert } from 'react-native';
import { launchImageLibrary, launchCamera } from 'react-native-image-picker';

const ImagePicker = () => {
  const [imageUri, setImageUri] = useState(null);

  const selectImage = () => {
    const options = {
      mediaType: 'photo',
      quality: 0.8,
    };

    launchImageLibrary(options, (response) => {
      if (response.didCancel) {
        console.log('User cancelled image picker');
      } else if (response.errorMessage) {
        console.log('ImagePicker Error: ', response.errorMessage);
      } else {
        setImageUri(response.assets[0].uri);
      }
    });
  };

  const takePhoto = () => {
    const options = {
      mediaType: 'photo',
      quality: 0.8,
    };

    launchCamera(options, (response) => {
      if (response.didCancel) {
        console.log('User cancelled camera');
      } else if (response.errorMessage) {
        Alert.alert('Camera Error', response.errorMessage);
      } else {
        setImageUri(response.assets[0].uri);
      }
    });
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="Select from Gallery" onPress={selectImage} />
      <Button title="Take Photo" onPress={takePhoto} />
      {imageUri && (
        <Image
          source={{ uri: imageUri }}
          style={{ width: 200, height: 200, marginTop: 20 }}
        />
      )}
    </View>
  );
};

2. AsyncStorage for Data Persistence

import AsyncStorage from '@react-native-async-storage/async-storage';

// Store data
const storeData = async (key, value) => {
  try {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  } catch (error) {
    console.error('Error storing data:', error);
  }
};

// Retrieve data
const getData = async (key) => {
  try {
    const value = await AsyncStorage.getItem(key);
    return value != null ? JSON.parse(value) : null;
  } catch (error) {
    console.error('Error retrieving data:', error);
  }
};

// Usage
const saveUserPreferences = async (preferences) => {
  await storeData('userPreferences', preferences);
};

const loadUserPreferences = async () => {
  const preferences = await getData('userPreferences');
  return preferences || { theme: 'light', notifications: true };
};

3. Network Requests

import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';

const DataFetching = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    try {
      setLoading(true);
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const refreshData = () => {
    fetchData();
  };

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" color="#007AFF" />
        <Text style={{ marginTop: 10 }}>Loading...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ color: 'red', marginBottom: 20 }}>
          Error: {error}
        </Text>
        <Button title="Retry" onPress={refreshData} />
      </View>
    );
  }

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' }}>
          <Text style={{ fontSize: 16, fontWeight: 'bold', marginBottom: 8 }}>
            {item.title}
          </Text>
          <Text style={{ color: '#666' }}>{item.body}</Text>
        </View>
      )}
      onRefresh={refreshData}
      refreshing={loading}
    />
  );
};

Performance Optimization

1. Memoization

import React, { useMemo, useCallback } from 'react';
import { View, Text, FlatList } from 'react-native';

const ExpensiveListItem = React.memo(({ item, onPress }) => {
  console.log('Rendering item:', item.id);

  return (
    <TouchableOpacity
      style={styles.item}
      onPress={() => onPress(item)}
    >
      <Text style={styles.title}>{item.title}</Text>
      <Text style={styles.body}>{item.body}</Text>
    </TouchableOpacity>
  );
});

const DataList = ({ data }) => {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processedTitle: item.title.toUpperCase(),
    }));
  }, [data]);

  const handleItemPress = useCallback((item) => {
    console.log('Pressed item:', item.id);
  }, []);

  const renderItem = useCallback(({ item }) => (
    <ExpensiveListItem
      item={item}
      onPress={handleItemPress}
    />
  ), [handleItemPress]);

  return (
    <FlatList
      data={processedData}
      renderItem={renderItem}
      keyExtractor={(item) => item.id.toString()}
    />
  );
};

2. List Optimization

import React from 'react';
import { FlatList, View, Text, TouchableOpacity } from 'react-native';

const OptimizedList = ({ data, onItemPress }) => {
  const renderItem = ({ item, index }) => (
    <TouchableOpacity
      style={styles.item}
      onPress={() => onItemPress(item)}
    >
      <Text style={styles.title}>{item.title}</Text>
      <Text style={styles.subtitle}>{item.subtitle}</Text>
    </TouchableOpacity>
  );

  const keyExtractor = (item) => item.id.toString();

  const getItemLayout = (data, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  });

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      initialNumToRender={10}
      maxToRenderPerBatch={10}
      windowSize={10}
      removeClippedSubviews={true}
      showsVerticalScrollIndicator={false}
    />
  );
};

Testing

Jest and React Native Testing Library:

npm install --save-dev @testing-library/react-native @testing-library/jest-native

Component Testing:

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react-native';
import Counter from '../Counter';

describe('Counter Component', () => {
  it('renders initial count', () => {
    render(<Counter />);
    expect(screen.getByText('Count: 0')).toBeTruthy();
  });

  it('increments count when button is pressed', () => {
    render(<Counter />);
    const incrementButton = screen.getByText('Increment');
    fireEvent.press(incrementButton);
    expect(screen.getByText('Count: 1')).toBeTruthy();
  });

  it('decrements count when button is pressed', () => {
    render(<Counter />);
    const decrementButton = screen.getByText('Decrement');
    fireEvent.press(decrementButton);
    expect(screen.getByText('Count: -1')).toBeTruthy();
  });

  it('resets count when reset button is pressed', () => {
    render(<Counter />);
    const incrementButton = screen.getByText('Increment');
    const resetButton = screen.getByText('Reset');

    fireEvent.press(incrementButton);
    fireEvent.press(incrementButton);
    expect(screen.getByText('Count: 2')).toBeTruthy();

    fireEvent.press(resetButton);
    expect(screen.getByText('Count: 0')).toBeTruthy();
  });
});

Deployment

1. iOS Deployment

Development Build:

npx react-native run-ios

Release Build:

# For Expo
expo build:ios

# For React Native CLI
cd ios && fastlane beta

2. Android Deployment

Development Build:

npx react-native run-android

Release Build:

# For Expo
expo build:android

# For React Native CLI
cd android && ./gradlew bundleRelease

3. Store Submission

App Store Connect (iOS):

  • Create app record
  • Upload build via Xcode or Fastlane
  • Configure metadata, screenshots, pricing
  • Submit for review

Google Play Console (Android):

  • Create app
  • Upload APK/AAB bundle
  • Configure store listing
  • Publish to production

Common Issues and Solutions

1. Metro Bundler Issues

  • Clear cache: npx react-native start --reset-cache
  • Clean node_modules: rm -rf node_modules && npm install
  • Reset Metro: npx react-native start --reset-cache

2. iOS Build Issues

  • Clean Xcode: Product → Clean Build Folder
  • Update CocoaPods: cd ios && pod install
  • Reset simulator: Device → Erase All Content and Settings

3. Android Build Issues

  • Clean Gradle: cd android && ./gradlew clean
  • Invalidate cache: File → Invalidate Caches / Restart
  • Update Android SDK and build tools

4. Performance Issues

  • Use Hermes engine for better performance
  • Implement proper key props for lists
  • Avoid unnecessary re-renders
  • Use FlatList instead of ScrollView for long lists
  • Implement proper image optimization

Best Practices

1. Code Organization

  • Use TypeScript for better type safety
  • Implement proper folder structure
  • Use custom hooks for reusable logic
  • Follow component composition patterns

2. UI/UX Guidelines

  • Follow platform-specific design guidelines
  • Implement proper touch targets (44pt minimum)
  • Use consistent spacing and typography
  • Handle different screen sizes gracefully

3. Error Handling

  • Implement proper error boundaries
  • Provide user-friendly error messages
  • Log errors for debugging
  • Implement retry mechanisms for network requests

4. Security

  • Validate user inputs
  • Use secure storage for sensitive data
  • Implement proper authentication flows
  • Keep dependencies updated

Future of React Native

React Native is continuously evolving:

  • Fabric Renderer: New architecture for better performance
  • TurboModules: Improved native module system
  • Codegen: Automatic native code generation
  • New Architecture: Concurrent React features integration

React Native remains a powerful choice for cross-platform mobile development, offering the perfect balance of development speed and native performance.

About the author

Rafael De Paz

Full Stack Developer

Passionate full-stack developer specializing in building high-quality web applications and responsive sites. Expert in robust data handling, leveraging modern frameworks, cloud technologies, and AI tools to deliver scalable, high-performance solutions that drive user engagement and business growth. I harness AI technologies to accelerate development, testing, and debugging workflows.

Tags:

Share: