AboutBlogContact
Mobile DevelopmentJuly 18, 2016 5 min read 132Updated: June 22, 2026

How to Build a Cross-Platform Mobile App with React Native 0.30+ in 2016

AunimedaAunimeda
📋 Table of Contents

Short answer: Use react-native init, install react-navigation for navigation, redux + react-redux for state, axios for HTTP. Shared code is ~85% - platform differences are mainly in push notifications (different APIs), native modules, and styling edge cases. Budget 20% extra time for Android-specific fixes.


Project Setup (React Native 0.30, 2016)

npm install -g react-native-cli
react-native init MyApp --version 0.30.0
cd MyApp

npm install --save redux react-redux
npm install --save react-navigation  # Replaced Navigator
npm install --save axios
npm install --save react-native-vector-icons

react-native run-ios
react-native run-android

App Structure

src/
├── App.js              ← Root component with navigation
├── store/
│   ├── index.js        ← Redux store
│   └── reducers/
│       ├── auth.js
│       └── orders.js
├── screens/
│   ├── LoginScreen.js
│   ├── HomeScreen.js
│   └── OrderDetailScreen.js
├── components/
│   ├── Button.js
│   └── OrderCard.js
├── services/
│   └── api.js          ← API calls
└── utils/
    └── platform.js     ← Platform-specific code

Navigation (react-navigation replaced Navigator in 2016)

// src/App.js
import React from 'react';
import { NavigationContainer } from 'react-navigation';
import { StackNavigator } from 'react-navigation';
import { Provider } from 'react-redux';
import store from './store';

import LoginScreen    from './screens/LoginScreen';
import HomeScreen     from './screens/HomeScreen';
import OrderDetail    from './screens/OrderDetailScreen';

const AppNavigator = StackNavigator({
  Login: {
    screen: LoginScreen,
    navigationOptions: { header: null }  // No header on login screen
  },
  Home: {
    screen: HomeScreen,
    navigationOptions: { title: 'My Orders' }
  },
  OrderDetail: {
    screen: OrderDetail,
    navigationOptions: ({ navigation }) => ({
      title: 'Order #' + navigation.state.params.orderId
    })
  },
}, {
  initialRouteName: 'Login',
  headerMode: 'screen',
});

export default () => (
  <Provider store={store}>
    <AppNavigator />
  </Provider>
);

Redux Store

// src/store/index.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';  // Middleware for async actions
import auth   from './reducers/auth';
import orders from './reducers/orders';

const rootReducer = combineReducers({ auth, orders });
const store       = createStore(rootReducer, applyMiddleware(thunk));

export default store;
// src/store/reducers/orders.js
const initialState = {
  list:    [],
  loading: false,
  error:   null,
};

export default function ordersReducer(state = initialState, action) {
  switch (action.type) {
    case 'ORDERS_FETCH_START':
      return { ...state, loading: true, error: null };
    case 'ORDERS_FETCH_SUCCESS':
      return { ...state, loading: false, list: action.payload };
    case 'ORDERS_FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

// Action creator (thunk)
export function fetchOrders() {
  return async (dispatch, getState) => {
    dispatch({ type: 'ORDERS_FETCH_START' });
    try {
      const { auth: { token } } = getState();
      const response = await api.get('/orders', { headers: { Authorization: 'Bearer ' + token } });
      dispatch({ type: 'ORDERS_FETCH_SUCCESS', payload: response.data.orders });
    } catch (error) {
      dispatch({ type: 'ORDERS_FETCH_ERROR', payload: error.message });
    }
  };
}

API Service

// src/services/api.js
import axios from 'axios';
import store  from '../store';

const api = axios.create({
  baseURL: 'https://api.myapp.com/v1',
  timeout: 10000,
});

// Interceptor: add auth token to every request
api.interceptors.request.use(config => {
  const { auth: { token } } = store.getState();
  if (token) {
    config.headers.Authorization = 'Bearer ' + token;
  }
  return config;
});

// Interceptor: handle 401 globally
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response && error.response.status === 401) {
      store.dispatch({ type: 'AUTH_LOGOUT' });
      // Navigation to login is handled by the App root component
    }
    return Promise.reject(error);
  }
);

export default api;

HomeScreen with List

// src/screens/HomeScreen.js
import React, { Component } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, RefreshControl } from 'react-native';
import { connect } from 'react-redux';
import { fetchOrders } from '../store/reducers/orders';

// Note: FlatList replaced ListView in RN 0.28 - much better performance
class HomeScreen extends Component {

  componentDidMount() {
    this.props.fetchOrders();
  }

  renderOrder = ({ item }) => (
    <TouchableOpacity
      style={styles.orderCard}
      onPress={() => this.props.navigation.navigate('OrderDetail', { orderId: item.id })}
    >
      <Text style={styles.orderId}>Order #{item.id}</Text>
      <Text style={styles.status}>{item.status}</Text>
      <Text style={styles.total}>${item.total}</Text>
    </TouchableOpacity>
  );

  render() {
    const { list, loading } = this.props;

    return (
      <View style={styles.container}>
        <FlatList
          data={list}
          keyExtractor={item => String(item.id)}
          renderItem={this.renderOrder}
          refreshControl={
            <RefreshControl
              refreshing={loading}
              onRefresh={this.props.fetchOrders}
            />
          }
          ListEmptyComponent={
            !loading && <Text style={styles.empty}>No orders yet</Text>
          }
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container:  { flex: 1, backgroundColor: '#f5f5f5' },
  orderCard:  { backgroundColor: '#fff', padding: 16, marginBottom: 1 },
  orderId:    { fontSize: 16, fontWeight: 'bold' },
  status:     { fontSize: 14, color: '#666', marginTop: 4 },
  total:      { fontSize: 14, color: '#007AFF', marginTop: 4 },
  empty:      { textAlign: 'center', marginTop: 48, color: '#999' },
});

const mapStateToProps    = state => ({ list: state.orders.list, loading: state.orders.loading });
const mapDispatchToProps = { fetchOrders };
export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);

Platform-Specific Code

// src/utils/platform.js
import { Platform, StyleSheet } from 'react-native';

// Shadow styles differ between iOS and Android
export const shadow = Platform.select({
  ios: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  android: {
    elevation: 3,  // Android uses elevation, not shadow
  },
});

// Platform-specific file: Button.ios.js / Button.android.js
// React Native will automatically pick the right one:
// import Button from './Button'
// → loads Button.ios.js on iOS, Button.android.js on Android

Push Notifications in 2016

// iOS: PushNotificationIOS (built into RN)
// Android: in 2016, react-native-gcm-android or react-native-push-notification

import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  PushNotificationIOS.requestPermissions(['alert', 'sound', 'badge']);
  PushNotificationIOS.addEventListener('notification', notification => {
    console.log('Received:', notification);
  });
}
// Android: GCM → FCM transition was happening in 2016
// FCM SDK released May 2016

iOS and Android Build

# iOS: open Xcode
open ios/MyApp.xcodeproj

# Android: generate keystore for release
keytool -genkey -v -keystore my-release-key.keystore \
  -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

# Android: assemble release
cd android && ./gradlew assembleRelease
# APK: android/app/build/outputs/apk/app-release.apk

Code Reuse in Practice (2016 Reality)

On our delivery app (iOS + Android, 2016):

Category Shared iOS-only Android-only
Business logic 100% 0% 0%
API calls 100% 0% 0%
State management 100% 0% 0%
UI components 82% 10% 8%
Push notifications 0% 50% 50%

Overall: ~85% code reuse. The 15% difference was mostly push notifications, platform-specific navigation styling, and a few camera/permissions APIs.

Build time: 8 weeks instead of the 13 we'd estimated for separate native apps.

Read Also

Flutter App Development in Bishkek: Cost, Timeline, and What to Expectaunimeda
Mobile Development

Flutter App Development in Bishkek: Cost, Timeline, and What to Expect

Everything you need to know about Flutter mobile app development in Bishkek, Kyrgyzstan in 2026: costs, timelines, team structure, and how to evaluate a development partner.

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)aunimeda
Mobile Development

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)

Push notifications are the single highest-ROI feature in mobile apps. Open rates are 7x higher than email. Here's how to implement them correctly in React Native - including background handling, deep linking, and analytics.

Flutter vs React Native in 2026: An Engineer's Honest Comparisonaunimeda
Mobile Development

Flutter vs React Native in 2026: An Engineer's Honest Comparison

Flutter and React Native both ship to iOS and Android from a single codebase. But they make radically different bets. Here's the concrete trade-offs - performance benchmarks, ecosystem, hiring market, and which one fits which product.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Mobile App Development

Get Consultation All articles