React Native Cross-Platform Development in Practice: One Codebase, Two Platforms
React Native lets you build native mobile applications using JavaScript and React, with a single codebase supporting both iOS and Android. This article walks you through the complete journey from environment setup to successful app store publication.
Why Choose React Native?
Advantages Analysis
Cross-Platform Efficiency:
- 85-95% code sharing rate
- One team maintaining both platforms
- 50% faster development speed
Native Performance:
- Renders using native components
- 60 FPS smooth animations
- Seamless native module integration
Rich Ecosystem:
- 300,000+ npm packages available
- Active community support
- Officially maintained by Meta
Over-the-Air Updates:
- Instant updates via CodePush
- No re-submission for app review required
- Rapid issue resolution
Environment Setup
Complete macOS Setup (Supporting iOS + Android)
# 1. Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. Install Node.js
brew install node
brew install watchman
# 3. Install React Native CLI
npm install -g react-native-cli
# 4. iOS development environment
# Install Xcode (from App Store)
sudo gem install cocoapods
# 5. Android development environment
brew install --cask android-studio
# Set Android SDK environment variables
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
# 6. Create a new project
npx react-native init MyAwesomeApp
cd MyAwesomeApp
# 7. Launch the project
# iOS
npx react-native run-ios
# Android
npx react-native run-android
Core Concepts and Components
Basic Component Library
// App.js - Demonstrating core components
import React, { useState } from 'react';
import {
SafeAreaView,
View,
Text,
Image,
TextInput,
Button,
TouchableOpacity,
FlatList,
ScrollView,
StyleSheet,
Alert,
} from 'react-native';
const App = () => {
const [text, setText] = useState('');
const [tasks, setTasks] = useState([]);
const addTask = () => {
if (text.trim()) {
setTasks([...tasks, { id: Date.now().toString(), title: text }]);
setText('');
}
};
const deleteTask = (id) => {
setTasks(tasks.filter(task => task.id !== id));
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>To-Do List</Text>
</View>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Enter a new task..."
value={text}
onChangeText={setText}
onSubmitEditing={addTask}
/>
<TouchableOpacity style={styles.addButton} onPress={addTask}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.taskItem}>
<Text style={styles.taskText}>{item.title}</Text>
<TouchableOpacity
onPress={() => deleteTask(item.id)}
style={styles.deleteButton}
>
<Text style={styles.deleteButtonText}>✕</Text>
</TouchableOpacity>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>No tasks yet. Add one!</Text>
}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 20,
backgroundColor: '#6200ee',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: 'white',
},
inputContainer: {
flexDirection: 'row',
padding: 16,
backgroundColor: 'white',
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 12,
fontSize: 16,
},
addButton: {
marginLeft: 8,
backgroundColor: '#6200ee',
width: 50,
height: 50,
borderRadius: 25,
alignItems: 'center',
justifyContent: 'center',
},
addButtonText: {
color: 'white',
fontSize: 28,
},
taskItem: {
flexDirection: 'row',
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginTop: 8,
borderRadius: 8,
alignItems: 'center',
},
taskText: {
flex: 1,
fontSize: 16,
},
deleteButton: {
padding: 8,
},
deleteButtonText: {
color: 'red',
fontSize: 20,
},
emptyText: {
textAlign: 'center',
marginTop: 50,
fontSize: 16,
color: '#999',
},
});
export default App;
Navigation System Implementation
React Navigation Setup
# Install React Navigation
npm install @react-navigation/native
npm install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
# Additional step for iOS
cd ios && pod install && cd ..
// Navigation.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
// Screen components
import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';
import SettingsScreen from './screens/SettingsScreen';
import DetailScreen from './screens/DetailScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
// Home Stack
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="HomeMain"
component={HomeScreen}
options={{ title: 'Home' }}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{ title: 'Details' }}
/>
</Stack.Navigator>
);
}
// Main Navigation
export default function Navigation() {
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 <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#6200ee',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeStack} options={{ headerShown: false }} />
<Tab.Screen name="Profile" component={ProfileScreen} options={{ title: 'Profile' }} />
<Tab.Screen name="Settings" component={SettingsScreen} options={{ title: 'Settings' }} />
</Tab.Navigator>
</NavigationContainer>
);
}
State Management
Redux Toolkit Implementation
npm install @reduxjs/toolkit react-redux
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import todosReducer from './todosSlice';
export const store = configureStore({
reducer: {
user: userReducer,
todos: todosReducer,
},
});
// store/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async operation: Fetch todos from API
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async (userId) => {
const response = await fetch(`https://api.example.com/todos?userId=${userId}`);
return response.json();
}
);
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle',
error: null,
},
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now().toString(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.failed, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;
// App.js
import { Provider } from 'react-redux';
import { store } from './store';
export default function App() {
return (
<Provider store={store}>
<Navigation />
</Provider>
);
}
API Integration
// services/api.js
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
const API_BASE_URL = 'https://api.example.com';
// Create axios instance
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
});
// Request interceptor (add token)
api.interceptors.request.use(
async (config) => {
const token = await AsyncStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor (handle errors)
api.interceptors.response.use(
(response) => response.data,
async (error) => {
if (error.response?.status === 401) {
// Token expired, redirect to login
await AsyncStorage.removeItem('authToken');
// Navigate to login screen...
}
return Promise.reject(error);
}
);
// API methods
export const authAPI = {
login: (email, password) =>
api.post('/auth/login', { email, password }),
register: (userData) =>
api.post('/auth/register', userData),
logout: () =>
api.post('/auth/logout'),
};
export const todosAPI = {
getAll: () =>
api.get('/todos'),
create: (todoData) =>
api.post('/todos', todoData),
update: (id, todoData) =>
api.put(`/todos/${id}`, todoData),
delete: (id) =>
api.delete(`/todos/${id}`),
};
export default api;
Native Module Integration
Camera and Image Picker
npm install react-native-image-picker
cd ios && pod install && cd ..
// components/ImagePickerComponent.js
import React, { useState } from 'react';
import { View, Button, Image, StyleSheet } from 'react-native';
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
export default function ImagePickerComponent() {
const [imageUri, setImageUri] = useState(null);
const options = {
mediaType: 'photo',
quality: 0.8,
maxWidth: 1024,
maxHeight: 1024,
};
const handleTakePhoto = () => {
launchCamera(options, (response) => {
if (response.didCancel) {
console.log('User cancelled photo capture');
} else if (response.errorCode) {
console.log('Error:', response.errorMessage);
} else {
setImageUri(response.assets[0].uri);
}
});
};
const handleSelectImage = () => {
launchImageLibrary(options, (response) => {
if (!response.didCancel && !response.errorCode) {
setImageUri(response.assets[0].uri);
}
});
};
return (
<View style={styles.container}>
{imageUri && (
<Image source={{ uri: imageUri }} style={styles.image} />
)}
<View style={styles.buttonContainer}>
<Button title="Take Photo" onPress={handleTakePhoto} />
<View style={styles.spacing} />
<Button title="Select Image" onPress={handleSelectImage} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
image: {
width: '100%',
height: 300,
borderRadius: 12,
marginBottom: 20,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
},
spacing: {
width: 10,
},
});
Performance Optimization
// Use React.memo to prevent unnecessary re-renders
const TodoItem = React.memo(({ item, onToggle, onDelete }) => {
return (
<View style={styles.item}>
<TouchableOpacity onPress={() => onToggle(item.id)}>
<Text style={item.completed ? styles.completed : styles.active}>
{item.text}
</Text>
</TouchableOpacity>
<Button title="Delete" onPress={() => onDelete(item.id)} />
</View>
);
}, (prevProps, nextProps) => {
// Custom comparison function
return prevProps.item.id === nextProps.item.id &&
prevProps.item.completed === nextProps.item.completed &&
prevProps.item.text === nextProps.item.text;
});
// Use useCallback to memoize callback functions
const App = () => {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<FlatList
data={todos}
renderItem={({ item }) => (
<TodoItem
item={item}
onToggle={handleToggle}
onDelete={handleDelete}
/>
)}
keyExtractor={item => item.id}
// Performance optimization configuration
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
/>
);
};
Publishing to App Stores
iOS App Store Submission
# 1. Update version number
# ios/MyApp/Info.plist
# CFBundleShortVersionString: 1.0.0
# CFBundleVersion: 1
# 2. Create Release Build
cd ios
xcodebuild archive -workspace MyApp.xcworkspace \
-scheme MyApp -archivePath build/MyApp.xcarchive
# 3. Export IPA
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build \
-exportOptionsPlist ExportOptions.plist
# 4. Upload to App Store Connect
# Use Transporter or Xcode to upload
Android Google Play Submission
# 1. Generate signing key
keytool -genkeypair -v -storetype PKCS12 \
-keystore my-release-key.keystore \
-alias my-key-alias \
-keyalg RSA -keysize 2048 -validity 10000
# 2. Configure gradle
# android/app/build.gradle
android {
signingConfigs {
release {
storeFile file('my-release-key.keystore')
storePassword 'YOUR_PASSWORD'
keyAlias 'my-key-alias'
keyPassword 'YOUR_PASSWORD'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
# 3. Create Release APK/AAB
cd android
./gradlew bundleRelease
# 4. Upload to Google Play Console
# File location: android/app/build/outputs/bundle/release/app-release.aab
Conclusion
React Native is one of the best choices for rapidly developing cross-platform mobile applications. Key success factors:
- Familiarity with the React Ecosystem - State management, Hooks, component architecture
- Mastering Native Integration - Understanding basic iOS/Android concepts
- Performance Optimization Awareness - Avoiding unnecessary re-renders
- Continuous Learning - React Native evolves rapidly
At BASHCAT, we have extensive React Native development experience and can help you rapidly build high-quality cross-platform applications. Feel free to contact us to discuss your app development needs!