1. Home

Anil Lakhman

< Blog />

Up and coding with React Native

in

Installation and setup

Final app skeleton

This is what we will create.

Skeleton app demo

Let’s build a native app with react native, we won’t use Expo, we’ll handle the bundling of native libraries ourselves to give us maximum control over our code base and our app.

We’ll integrate the following libraries.

  • React / Redux
  • React Native Vector Icons
  • React Native Navigation
  • React Native Splash Screen

Tip

  • Double click on code blocks to automatically select all text within in.
  • Replace NEW_PROJECT_NAME='YourProject' with your project name so you can following along.
  • You should be able to copy and paste all the commands below straight into your terminal to create all the files as required.
Store our project name in a variable
NEW_PROJECT_NAME='AwesomeProject'
Init & run the project & open a working env
# Install react-native-cli helper
npm install -g react-native-cli

# Traverse to your project directory
cd ~/xCode

# Init our project (the directory will be created for you)
react-native init $NEW_PROJECT_NAME

# Open our project in webstorm
wstorm $NEW_PROJECT_NAME

# Open a new terminal and run the following
cd $NEW_PROJECT_NAME
react-native run-ios

# I won't show android, but you can start it using the following
# react-native run-android

You will see a packager open in a new terminal window, keep this open, it runs our development server which serves the bundle to react-native on the device.

Everything should compile and you should see your new project in the iOS simulator.

React native starter project

If you’re having any issues, try updating your xcode. You can also manually boot your app using the following:

# Manually start our packager - react native uses yarn, so we'll use that from now on.
yarn run start

# Open your project in xcode, and build and run manually on your selected device
open ios/*.xcodeproj/

This is all from the react native docs: https://facebook.github.io/react-native/docs/getting-started.html



React Native Vector Icons

A simple way to get started with vector icons in react native (for both iOS and android) is react-native-vector-icons.

Install and link the library
# Add it via yarn
yarn add react-native-vector-icons

# Link the native library automatically
react-native link

# Recompile your application (we installed the fonts and library which must be copied natively)
# If you look inside Project > Build Phases > Copy Bundle Resources you can see our icons

Tip

If you’re having any issues with integrating packages check the manual instructions on the github page, react-native link can sometimes fail and your app will fail to compile. You can fix these by manually checking your header search paths, link libraries and anything else specified by the package instructions.


React Native Navigation

We need to install react-native-navigation, we use this to achieve a truly native navigation experience rather than animating the navigation on the main javascript thread.

Install and link the library
# Install it
yarn add react-native-navigation@latest
# yarn add react-native-navigation#v2

# Use react native link to link it (or do this manually in xcode)
# https://wix.github.io/react-native-navigation/#/installation-ios
react-native link

# Make a new directory 'src' to hold our source files
mkdir src

# Create a package.json so we can reference this directory by name
# e.g    : import rootReducer from '../components/demo';
# becomes: import rootReducer from 'src/components/demo';
cat <<'-EOF' > src/package.json
{ "name": "src" }
-EOF

We need to update our app entry point so we can use react native navigation, in your AppDelegate.m remove the rootViewController and all corresponding lines, it should look like the following.

Update AppDelegate.m with the following changes
#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

#import "RCCManager.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  #ifdef DEBUG
    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #else
    jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
  #endif

  // Removed all rootViewController code

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  [[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions];
  return YES;
}

If you have any issues with recompiling, complete the steps described here (manually link your app).

Before we can run our application, we need to provide the screens to use.

Let’s create a HomeScreen to begin with.

Create a home screen

We need to create our home screen component. You can copy and paste the following into your terminal to create it in your project.

Create the src/containers/HomeScreen container component
# Create our screens
mkdir -p src/containers/HomeScreen

# Our home screen container has 3 files (index, styles, and container component)

# 1. Our index file to export our component
cat <<'-EOF' > src/containers/HomeScreen/index.js
import HomeScreen from './HomeScreen';
export default HomeScreen;
-EOF

# 2. Our styles file to hold the styles of our component
cat <<'-EOF' > src/containers/HomeScreen/styles.js
import { StyleSheet, Platform } from 'react-native';

export default StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    instructions: {
        margin: 10,
    },
});
-EOF

# 3. Our component file to hold the main container component
cat <<'-EOF' > src/containers/HomeScreen/HomeScreen.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import styles from './styles'

type Props = {};
export default class HomeScreen extends Component<Props> {
    render() {
        return (
            <View style={styles.container}>
                <Text style={styles.instructions}>
                    This is the home screen
                </Text>
            </View>
        );
    }
}
-EOF

Create a data screen

We need to create a data screen component. You can copy and paste the following to create it in your project.

Create src/containers/DataScreen container
# Create our screens
mkdir -p src/containers/DataScreen

# Our data screen container has 3 files (index, styles, and container component)

# 1. Our index file to export our component
cat <<'-EOF' > src/containers/DataScreen/index.js
import DataScreen from './DataScreen';
export default DataScreen;
-EOF

# 2. Our styles file to hold the styles of our component
cat <<'-EOF' > src/containers/DataScreen/styles.js
import { StyleSheet, Platform } from 'react-native';

export default StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
});
-EOF

# 3. Our component file to hold the main container component
cat <<'-EOF' > src/containers/DataScreen/DataScreen.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import styles from './styles'

type Props = {};
export default class DataScreen extends Component<Props> {
    render() {
        return (
            <View style={styles.container}>
                <Text style={styles.instructions}>
                    This is the data screen
                </Text>
            </View>
        );
    }
}
-EOF

Register our screens

Register screens in src/screens.js
cat <<'-EOF' > src/screens.js
import { Navigation } from 'react-native-navigation';

import HomeScreen from './containers/HomeScreen';
import DataScreen from './containers/DataScreen';

// register all screens of the app (including internal ones)
export function registerScreens() {
  Navigation.registerComponent('rn.HomeScreen', () => HomeScreen);
  Navigation.registerComponent('rn.DataScreen', () => DataScreen);
  Navigation.registerComponent('rn.FirstDrawerScreen', () => HomeScreen);
  Navigation.registerComponent('rn.PushedScreen', () => HomeScreen);
}
-EOF

Update our app entry to use React Native Navigation

We update our app’s entry point to use RNN.

Update index.js to start our app using RNN with icons
cat <<'-EOF' > index.js
import { Navigation } from 'react-native-navigation';
import { registerScreens } from './src/screens';
import Ionicons from 'react-native-vector-icons/Ionicons';

/**
 * Register all the screens our app will use (internal and external)
 */
registerScreens();

/**
 * When loading icons, we have to load them via a promise.
 * We must wait for the promise to be resolved before we can start our app
 * This happens very quickly so you don't notice it.
 */
const replaceSuffixPattern = /--(active|big|small|very-big)/g;
const icons = {
    'ios-home': [30, '#000000'], 'ios-home-outline': [30, '#bbb'],
    'ios-list': [30, '#000000'], 'ios-list-outline': [30, '#bbb'],
};

const iconsMap = {};
const iconsLoaded = new Promise((resolve, reject) => {
    new Promise.all(
        Object.keys(icons).map(iconName =>
            // IconName--suffix--other-suffix is just the mapping name in iconsMap
            Ionicons.getImageSource(
                iconName.replace(replaceSuffixPattern, ''),
                icons[iconName][0],
                icons[iconName][1]
            ))
    ).then(sources => {
        Object.keys(icons).forEach((iconName, idx) => (iconsMap[iconName] = sources[idx]));
        resolve(true); // Call resolve all our icons exist in the iconMap
    });
});

/**
 * Once our icons have loaded, then start our app
 */
iconsLoaded.then(() => {
    // Start the app
    Navigation.startTabBasedApp({
        tabs: [
            {
                title: 'Home Screen',
                label: 'HomeScreen',
                screen: 'rn.HomeScreen', // this is a registered name for a screen
                icon: iconsMap['ios-home-outline'],
                selectedIcon: iconsMap['ios-home'],
            },
            {
                title: 'Data Screen',
                label: 'Data',
                screen: 'rn.DataScreen',
                icon: iconsMap['ios-list-outline'],
                selectedIcon: iconsMap['ios-list']
            }
        ]
    });
});
-EOF
Remove App.js now we’re using our container
rm App.js

Create a Splash Screen

When our app starts, we see the default plain splash screen with the text “Powered by React Native”.

Let’s use react native splash screen to add our own custom splash screen and also dismiss it through javascript.

Install & Link react-native-splash-screen
yarn add react-native-splash-screen
react-native link react-native-splash-screen

Next we need to update our AppDelegate.m to include the SplashScreen library and have it continue displaying our splash screen before didFinishLaunchingWithOptions returns.

Update AppDelegate.m to display the splash
#import "SplashScreen.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  [SplashScreen show];
  return YES;
}

Inside your main index.js boot file, add the SplashScreen.hide(); after the icons promise has resolved.

index.js
import SplashScreen from 'react-native-splash-screen'

iconsLoaded.then(() => {
    SplashScreen.hide();

    // Boot Navigation
})

Open xCode and design your splash screen in the LaunchScreen.xib, use Add files to project to add images to your project and then drag an image view from the object library to your splash and reference your image within it’s properties.


Redux

What’s a react app without redux. Let’s set that up next.

We’ll be using redux, redux logger, redux-thunk, redux-immutable-state-invariant.

Install dependencies
# redux (our beloved redux)
# react-redux (official react bindings)
# redux-thunk (dispatch async actions)
# redux-logger (DEV: verbose logging for development)
# redux-immutable-state-invariant (DEV: throws an error for mutating state in dispatch)
yarn add redux react-redux redux-thunk redux-logger redux-immutable-state-invariant

Next we need to create our redux store files, we need a store, actions and reducers directory.

Create directories
mkdir -p src/store
mkdir -p src/actions
mkdir -p src/reducers
Create src/store/configureStore.js
cat <<'-EOF' > src/store/configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from 'src/store/rootReducer';

let middleware = [thunk];

if (__DEV__) {
    const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
    middleware = [...middleware, reduxImmutableStateInvariant, logger];
} else {
    middleware = [...middleware];
}

export default function configureStore(initialState) {
    return createStore(
        rootReducer,
        initialState,
        applyMiddleware(...middleware)
    );
}
-EOF

Update index.js to initialize the store at the top and pass it and our Provider to registerScreens.

In index.js add the store
// Import our store and redux provider
import configureStore from 'src/store/configureStore';
import { Provider } from 'react-redux';

// Configure our store
const store = configureStore();

/**
 * Register all the screens our app will use (internal and external)
 * React Native Navigation automatically wraps the component for us using the provider and store
 */
registerScreens(store, Provider);

Update src/screens.js to use our store and provider, react native navigation will initialize our provider and store for us for every screen we create.

Pass the store and provider to RNN
cat <<'-EOF' > src/screens.js
import { Navigation } from 'react-native-navigation';

import HomeScreen from './containers/HomeScreen';
import DataScreen from './containers/DataScreen';

// register all screens of the app (including internal ones)
export function registerScreens(store, Provider) {
  Navigation.registerComponent('rn.HomeScreen', () => HomeScreen, store, Provider);
  Navigation.registerComponent('rn.DataScreen', () => DataScreen, store, Provider);
  Navigation.registerComponent('rn.FirstDrawerScreen', () => HomeScreen, store, Provider);
  Navigation.registerComponent('rn.PushedScreen', () => HomeScreen, store, Provider);
}
-EOF

Create our actions

Actions are payloads of information that send data from your application to your store.

Create actions/index.js to export all actions
cat <<'-EOF' > src/actions/index.js
import * as AppActions from './app';

// Merge all our actions together and return all creators
export const ActionCreators = Object.assign({},
    AppActions,
);
-EOF
Create actions/types.js to hold action types
cat <<'-EOF' > src/actions/types.js
// App wide actions
export const SET_APP_LOADING = 'SET_APP_LOADING';
-EOF
Create actions/app.js to hold app actions
cat <<'-EOF' > src/actions/app.js
import * as types from './types';

/**
 * Trigger a loading state (to show an indicator)
 *
 * @param loading bool true or false
 * @returns {{type, loading: *}}
 */
export function setAppLoading(loading) {
    return {
        type: types.SET_APP_LOADING,
        loading,
    };
}
-EOF

Create our reducers

A reducer is a pure function that takes the previous state and an action and returns the new state.

Create reducers/index.js to export all reducers
cat <<'-EOF' > src/reducers/index.js
import { combineReducers } from 'redux';
import appReducer from './app';

// Combine all our reducers
// The keys we define here will be used in our component as "this.props.app.loading"
export default combineReducers({
    app: appReducer
});
-EOF
Create reducers/app.js to hold app reducers
cat <<'-EOF' > src/reducers/app.js
import * as types from 'src/actions/types';

export default function appReducer(state = {loading: false}, action) {
    switch (action.type) {
        case types.SET_APP_LOADING:
            return {
                ...state,
                loading: action.loading
            };
        default:
            return state
    }
}
-EOF

Update the container component

We need to update the container component to use redux, I’ll show the HomeScreen component here, you should update the DataScreen and every other component the same way.

Update HomeScreen to use redux store
cat <<'-EOF' > src/containers/HomeScreen/HomeScreen.js
import React, { Component } from 'react';
import { ActivityIndicator, Text, View, TouchableOpacity } from 'react-native';
import styles from './styles'
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { ActionCreators } from "src/actions";

type Props = {};
class HomeScreen extends Component<Props> {
    render() {
        return (
            <View style={styles.container}>
                <View style={{height: 50}}>
                    {this.props.app.loading && <ActivityIndicator size="large" color="#000000" />}
                </View>
                <Text style={styles.instructions}>
                    Home screen {this.props.app.loading === true ? 'is loading' : 'is not loading'}
                </Text>
                <TouchableOpacity onPress={() => this.props.actions.setAppLoading(!this.props.app.loading)}>
                    <Text>Toggle Loading</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

function mapStateToProps(state, ownProps) {
    return {
        ...state,
    };
}

function mapDispatchToProps(dispatch) {
    return {
        actions: bindActionCreators(ActionCreators, dispatch)
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
-EOF

App skeleton

We created simple bare-bone skeleton for our app.

Skeleton app demo
  • React Native Vector Icons
  • React Native Navigation
  • React Native Splash Screen
  • Redux


Troubleshooting React Native Development

You’ll inevitably run into issues when developing with react native, sometimes it was as simple as restarting the package manager (which you do whenever you install a new package), other times it was react native link (which didn’t link everything), always manually check native links if you’re having any compiling issues.

Below are some of the issues I had and things I learned.

Debugging

For testing on your device, make sure your on the same network and modify your AppDelegate.m as shown here (add the IP instead of localhost).

https://gist.github.com/almost/898a829d5197c69d29b0

Debugging Production Errors

If you have errors in a react native app, you’ll usually see a well described error and stack trace in your development environment, in production this is more difficult as you don’t have access to the development tools.

In this case, you can use libimobiledevice, libimobiledevice allows you to stream the logs from your iOS device to your terminal for debugging. This streams all logs (even that of the iOS device itself).

You can filter through these and find the cause as to why your application crashed.

This works in production apps as well so it’s awesome for those pesky production only bugs.

Debugging iOS
# Install app logger (so we can log on our real device ipad)
brew install libimobiledevice

# you might need to do this
sudo chmod 777 /var/db/lockdown

# pair the device (tap trust on the ipad)
idevicepair pair

# !! dumps all the logs as they happen!! close the app, start it, all logs will output to terminal
idevicesyslog

# <Critical>: Attempting to change configurable attribute of unconfigurable property.
# <Critical>: Module AppRegistry is not a registered callable module (calling runApplication)

Attempting to change configurable attribute of unconfigurable property.

If you’re getting this error, you are probably trying to set the name of a property for which you can’t.

Error: attempting to change configurable attribute of unconfigurable property.

For example, you can replicate this error using the following code:

class SomeClass {
    static name() {
        // attempting to change configurable attribute of unconfigurable property.
        return 'error'
    }
}

// the above and below with throw the same error.

Object.defineProperty(window, 'name', {
    value: {},
    configurable: true,
    enumerable: true,
});

You can fix this 2 ways:

  • Create the method a regular method by removing the static
  • rename the static name() method to something else like static username()

Date parsing

Use moment.js for date parsing, I had issues with date-fns in react native the library worked in development, but not production. Switching to moment js fixed all date issues.

Expo

I had an app which used expo. An expo app was around 60mb in app size, a raw react native app (like the one we built above) is around 6mb!. Expo is a way of writing native apps in javascript only, all the native modules are already bundled in the app for you. This explains why the final binary is so large.