Blog

Build your own Instagram using React Native

React and React Native have been out for awhile now. They are here to stay and have been a total game changer. If you look at this blog post on StackOverflow about the job trends for this year, it confirms that knowing React is a must if you want to keep up with current tech trends and best practices. This is why I decided to learn React, and, because most of the things I’ve been doing for the last 3 years are mobile apps, I decided to go with React Native.

I’ve been doing software for almost 9 years now. This is not the first time I’ve had to onboard a new technology/framework/you name it. Every time I’m about to start a process like this a little voice inside my head begs me: “Please, not another TODO app, please”. This is why I decided to do something a little bit different this time. I thought: “Let’s learn React Native doing something fun! Something that actually involves some sort of real use cases or things that I could be implementing on my first/next React Native app.”

What are we building?

In this blog post, we are going to build “Gorillagram”, our own kind of Instagram app. We are going to use Cloudinary as our CDN Cloud Service to store and retrieve the images (I encourage you to check their services, they are really great and it could be very useful in your next projects).

Our app is going to let users post images, tag them (at least one tag will be required), attach the location where the image was taken (this will be optional) and then search images by tag.

gorillagram

Also, on the React Native side, we are going to be covering/implementing things like:

  • Redux
  • Navigation (with Redux)
  • Pulling data as well as posting data to Cloudinary
  • Applying customizations per platform
  • Geolocation and maps
  • Working with the photo gallery and the camera
  • Supporting different languages in the app

The current version of the code is here. Feel free to clone it, fork it or even send pull requests! There you will also find some useful tips and information. Now, let’s start the journey!

Redux

React alone only covers the view layer of the app, so you need to figure out a way to support your workflows and your logic. That’s where Redux comes into play as a convenient state container. If you are used to working with things like MVC or MVVM, you will find that Redux is a little bit different.

Basically Redux handles a state. Its scope is the whole application and it wraps all your data. This state is handled by a global store. You can then start to create your components and bind them to the parts of the state that concern it. Finally, there are ‘actions’ that could change the state, and as a side effect of this, update your view components. One last important concept in Redux are ‘reducers’, which are small functions to generate a new state piece given a data input (usually provided as the result of an action).

I like to think about Redux as a pattern based on the “cause and effect” principle. The actions are the cause of a state change, and therefore, a change on the view.

To start implementing Redux, we are going to include the libraries (via npm):

  • redux
  • react-redux
  • redux-logger
  • redux-thunk

Defining the actions

The first thing I like to do is to define a module with all the available actions into the app. This way we can store and reference all actions on and from one single place. The app/actions/types.js file looks like this:

//Application
export const SET_IS_APP_WORKING = 'SET_IS_APP_WORKING';
export const SET_LANGUAGE = 'SET_LANGUAGE';

//Navigation
export const NAVIGATION_NAVIGATE = 'Navigation/NAVIGATE';
export const NAVIGATION_BACK = 'Navigation/BACK';

//Images actions
export const SET_SEARCHED_IMAGES = 'SET_SEARCHED_IMAGES';
export const SET_CURRENT_SEARCH_TAG = 'SET_CURRENT_SEARCH_TAG';
export const IMAGE_UPLOADED = 'IMAGE_UPLOADED';

Then we can start to actually implement our actions. I’m going to use the action to fetch images as an example:

import * as types from './types';
import Api from '../lib/api';
import * as config from '../config';
import * as apiUtils from '../utils/api';

export function fetchImages(tag) {
    return (dispatch, getState) => {
        return Api.get(`${config.CDNUriBase}/${config.CDNCloudName}/image/list/${encodeURIComponent(tag)}.json`)
            .then((response) => {
                dispatch(setSearchedImages({ images: response.resources }));
            });
    }
}

export function setSearchedImages({ images }) {
    return {
        type: types.SET_SEARCHED_IMAGES,
        images
    };
}     

This code snippet calls the Cloudinary API to fetch images given a tag and, once the result is back, it dispatches the SET_SEARCHED_IMAGES with the found images data.

Reducers

In Redux, once an action is dispatched, reducers handles the action, “transform” the received input and, finally, produces a new piece of the state. This step “refreshes” the state and, as a side effect, React updates all the components that are bound to the specific piece of the state which has been changed. You can find the reducer which is in charge of the images in the app/reducers/images.js:

import createReducer from '../lib/createReducer';
import * as types from '../actions/types';

export const searchedImages = createReducer({}, {

    [types.SET_SEARCHED_IMAGES](state, action) {
        let newState = {};
        action.images.forEach(image => {
            newState[image.public_id] = image;
        });
        return newState;
    }
});     

The store

Now that we have actions and reducers, we are ready to go to the store. I like to isolate the creation of the store in one single module. You will find it in the app/store.js:

import { AsyncStorage } from 'react-native';
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import createLogger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducers';

const loggerMiddleware = createLogger({ predicate: (getState, action) => __DEV__ });

function configureStore(initialState) {
    const enhancer = compose(
        applyMiddleware(
            thunkMiddleware,
            loggerMiddleware,
        )
    );
    return createStore(reducer, initialState, enhancer);
}

//Create the store (as a singleton, this module will always return this same instance)
const store = configureStore({});

export default store;

The main things we are doing with this code are:

  • Combining and importing all the reducers to include them in the store
  • Enhancing our store with two middlewares: thunk and logger (added only in development mode)

Now that we have the store, we can use it to create our Redux Provider. Both our index.js files (one for each platform supported: iOS and Android) look like this:

import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { AppRegistry } from 'react-native';
import NavigationContainer from './app/containers/navigation/NavigationContainer';
import store from './app/store';

class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <NavigationContainer />
            </Provider>
        );
    }
}

AppRegistry.registerComponent('Gorillagram', () => App);    

Navigation (with Redux)

Navigation has been one of the most discussed topics on React Native since day one. There are several libraries trying to solve the problem. After digging around,  I found react-navigation library works best for our purposes, especially because it includes a stack implementation and supports integration with Redux out of the box.

Remember when we created our Redux provider in our index.js files? Right under it, we are going to place our NavigationContainer component, which is basically a wrapper of the StackNavigator provided by the library. We are wrapping the StackNavigator and not using it directly, just to support a couple of features that I will explain in more depth later. Our NavigationContainer component will look something like this:

import React, { Component } from 'react';
import { addNavigationHelpers } from 'react-navigation';
import { View, StyleSheet } from 'react-native';
import AppNavigator from './AppNavigator';
import ProgressSpinner from '../../components/ProgressSpinner';

export default class NavigationContainerBase extends Component {

    /**
    * Virtual
    */
    marginTop () {
        return 0;
    }

    render() {
        const navHelpersConfig = {
            dispatch: this.props.dispatch,
            state: this.props.nav
        };
        return (
            <View style={[styles.container, { marginTop: this.marginTop() }]}>
                {
                    this.props.language &&
                    <AppNavigator navigation={addNavigationHelpers(navHelpersConfig)} ref={(o) => this.appNavigator = o} />
                }
                {
                    this.props.isAppWorking && <ProgressSpinner />
                }
            </View>
        );
    }

    static mapToStateProps(state) {
        return {
            nav: state.nav,
            language: state.language,
            isAppWorking: state.isAppWorking
        }
    }
}      

At this point the most important thing to explain is how to create the AppNavigator component using the helpers also provided by the library. This helper allows us to propagate the dispatch function and the navigation state to all the inner views that we are going to be creating and stacking as long as the user navigates into the app.

This way you will be able to navigate from one view to another. Let’s use the navigation from our Home view to our ImageEditor view as an example. In our app/views/Home.js file you will find this code which allows us support the navigation:

onImageDataResolved(imageData) {
    this.props.navigation.navigate('ImageEditor', { imageData });
}    

The navigation component is attached to our views props because the library supports this. The only thing we need to do to get this working is to create reducers to handle ‘push’ and ‘pop’ actions. Check our app/reducers/nav.js file:

import createReducer from '../lib/createReducer';
import * as types from '../actions/types';
import AppNavigator from '../containers/navigation/AppNavigator';
import { NavigationActions } from 'react-navigation';

const initialNavState = {
index: 0,
routes: [
    { key: 'initHome', routeName: 'Home' }
]
};

export const nav = createReducer(initialNavState, {
    [types.NAVIGATION_NAVIGATE](state, action) {
        const newState = AppNavigator.router.getStateForAction(action, state);
        if(action.params) {
            newState.params = action.params;
        }
        return newState;
    },
    
    [types.NAVIGATION_BACK](state, action) {
        return AppNavigator.router.getStateForAction(NavigationActions.back(), state);
    }
});      

Android back button support

As you might notice, our NavigationContainer implementation actually uses a NavigationContainerBase component which abstracts pretty much everything we need. But, it also gives us space to create platform specific containers which are going to inherit from this base.

When it’s about navigation this is very helpful because we are going to be using the app/containers/navigation/NavigationContainer.android.js component to expand our initial implementation for Android devices and adding support to the native back button.

We are adding a listener to the hardware back button and just checking if there are stacked views when the user hits it. If there are stacked views, we just fire a goBack action to go back to the previous view and return true to let React Native know the event was handled by us. If there are no stacked views, we just return false to not handle the event and bubble it to the device itself. Our code looks like this:

import React from 'react';
import { BackAndroid } from 'react-native';
import { connect } from 'react-redux';
import NavigationContainerBase from './NavigationContainerBase';

class NavigationContainer extends NavigationContainerBase {

    constructor(props) {
        super(props);

        //Add support to hardware back button
        BackAndroid.addEventListener('hardwareBackPress', () => {
            const currentNavState = this.appNavigator.props.navigation.state;
            if(currentNavState.index) {
                this.appNavigator.props.navigation.goBack();
                return true;
            }
            return false;
        });
    }
}

export default connect(NavigationContainerBase.mapToStateProps)(NavigationContainer);

Customizations per platform

There are two ways to apply customizations per platform in React Native. The first one is using the Platform module which lets you detect the current platform and apply customizations explicitly; like this:

import { Platform, StyleSheet } from 'react-native'; const styles = StyleSheet.create({    height: (Platform.OS === 'ios') ? 200 : 100, });    

The second way (which I prefer) is using a pattern of file names including the platform you want to customize. Here we are going to use our NavigationComponent as an example. As you may know, iOS devices don’t isolate their native top status bar from the whole viewport you have available to use when you are building your apps. This status bar has a height of 20px and there is a big chance you have to arrange the top of your app by setting a margin top yourself. This applies only to iOS, not to Android, so, here we are requiring custom code for iOS.

(NOTE: there are newer features and implementations regarding status bars for both platforms so there is a chance that you will need more complex code to deal with status bars in the future)

Lets see our code. If you go back to our code base, you will find that we have a app/containers/navigation/NavigationContainerBase.js file and you can see its code looks like this:

export default class NavigationContainerBase extends Component {    
/**    * Virtual    */    marginTop () {        
return 0;    }    render() {        
return (            
<View style={{ marginTop: this.marginTop() }}>               
 <AppNavigator navigation={addNavigationHelpers(navHelpersConfig)} ref={(o) => this.appNavigator = o} />          
   </View>        );    
   } }       

The virtual tag on a function or a property is a hint which means it can be overriden by any child class. Following our example, in the app/containers/navigation/NavigationContainer.ios.js file you will find this:

import React from 'react'; 
import { connect } from 'react-redux'; 
import NavigationContainerBase from './NavigationContainerBase'; 
class NavigationContainer extends NavigationContainerBase {    marginTop () {        
return 20;    } } 
export default connect(NavigationContainerBase.mapToStateProps)(NavigationContainer);      

In this child class, we are using the file extension pattern supported by React Native out of the box to apply customizations for iOS devices, in this case, a margin-top of 20 to the main app view wrapper. The Android version doesn’t override this function so it keeps the default margin-top of zero.

Maps

As the official React Native documentation states, when it is about maps the best thing you can do is to use the component built by Airbnb. If you have previous experience working with maps using javascript libraries, you will find this component very easy to use as it keeps several similarities with the work we have done before with maps on web and hybrid ecosystems.

Working with the photo gallery and camera

Our implementation of the interaction with both photo gallery and camera relies on these two libraries:

  • react-native-actionsheet
  • react-native-image-picker

First we are going to use react-native-actionsheet to let the user to select what kind of source he/she wants to use to upload a new image: photo gallery or camera.

The configuration of the action sheet in the app/views/Home.js file looks like this:

.... import ActionSheet from 'react-native-actionsheet'; class Home extends Component {    render() {        return (            <View style={viewStyles.viewContainer}>                ...                <ActionSheet                    ref={(o) => this.addImageActionSheet = o}                    options={this.props.language.home.addImageOptions}                    cancelButtonIndex={Home.AddImageCancelButtonIndex}                    onPress={this.onAddImageActionSheetPress.bind(this)}                />            </View>        )    }    onAddImage() {        this.addImageActionSheet.show();    }    onAddImageActionSheetPress(index) {        if(index !== Home.AddImageCancelButtonIndex) {            const methodName = (index === Home.AddImageFromGalleryButtonIndex) ? 'launchImageLibrary' : 'launchCamera';
            ImagePicker[methodName]({}, response => {                if(response.error) {                    console.log(response.error);                    return;                }                !response.didCancel && response.data && this.onImageDataResolved(response);            });        }    } }         

Let's explain a couple of things:

  • ref={(o) => this.addImageActionSheet = o}: we are saving a reference of the action sheet itself which is going to let us to show it later by doing this:onAddImage() {    this.addImageActionSheet.show(); }                    
  • options={this.props.language.home.addImageOptions}: options config expects an array of strings but since our app is localized we reference the current languages config (we are going to explain this deeper in the next section)

With the action sheet in place, it’s time to use react-native-image-picker to pick an image from the photo gallery or trigger the camera to take a new image from scratch. Based on the user selection we are going to be calling launchImageLibrary or launchCamera provided by the library. At the end, both functions will be returning the same result: the base 64 representation of the image and that’s what we are going to be posting to Cloudinary.

In our app/views/Home.js file our code to trigger the image library is:

class Home extends Component {    ...    onAddImageActionSheetPress(index) {        if(index !== Home.AddImageCancelButtonIndex) {            const methodName = (index === Home.AddImageFromGalleryButtonIndex) ? 'launchImageLibrary' : 'launchCamera';            ImagePicker[methodName]({}, response => {                if(response.error) {                    console.log(response.error);                    return;                }                !response.didCancel && response.data && this.onImageDataResolved(response);            });        }    } }          

Once we have the data of the image (base 64 representation), we navigate to the app/views/ImageEditor.js view by this:

class Home extends Component {    ...    onImageDataResolved(imageData) {        this.props.navigation.navigate('ImageEditor', { imageData });    } }         

This view is the place where the user will be able to input some extra data regarding the picture like tags (at least one required), a caption, and the location where the picture was taken (optional). It looks like this:

Supporting different languages in the app

There is a big chance your user base will be composed of different cultures. That’s why supporting different languages in your app could be a key functional requirement.

The first thing we are going to do is to create a module for each language we want to support. In this case, Gorillgram supports two languages: English and Spanish. Our English module in app/config/languages/en-US.js looks like this (The Spanish module has exactly the same structure but uses a different set of translations):

export default en_US = {
    languageKey: 'en-US',
    home: {
        searchByTag: 'Search by tag',
        addImageOptions: [
            'Gallery',
            'Camera',
            'Cancel'
        ]
    },
    feed: {
        noImagesToDisplay: 'No images to display',
    },
    imageEditor: {
        tagsPlaceholder: 'tags (at least one required)',
        captionPlaceholder: 'caption (optional)',
        includeLocation: 'Include location'
    },
    settings: {
        languagesOptions: [
            'English',
            'Spanish',
            'Cancel'
        ]
    }
};

Once we have all the modules matching each language we want to support, we create a new module at app/config/languages/index.js to export all our languages as a collection (this will make our code changing languages very easy):

import en_US from './en-US';
import es_CR from './es-CR';

export default languagesConfig = [
    en_US,
    es_CR
];

With the configuration in place, now we need to include our language support into the “Redux scope”. The first thing to do is to add the current language to the store. This will be done by creating a reducer, which is going to let us eventually “transform” our state. Check app/reducers/app.js:

import createReducer from '../lib/createReducer'; import * as types from '../actions/types'; ... export const language = createReducer(null, {    [types.SET_LANGUAGE](state, action) {        return action.language;    } });

Next, we create an action that is going to let us set the language. It’s worth mentioning that this action will persist the language id to reuse it when the user comes back to the app later. Check app/actions/app.js:

import createReducer from '../lib/createReducer';
import * as types from '../actions/types';

...

export const language = createReducer(null, {
    [types.SET_LANGUAGE](state, action) {
        return action.language;
    }
});

To fire this action and actually change the language, we’ve created a Settings component in app/components/Settings.js, where again you will find an action sheet showing all the available languages and once the user picks one, the action will be fired.

...
class Settings extends Component {

    onPress() {
        this.settingsActionSheet.show();
    }

    onSettingsActionSheetPress(index) {
        const language = languagesConfig[index];
        language && this.props.setLanguage(language);
    }

    render() {
        return (
            <View>
                <IconButton onPress={this.onPress.bind(this)} icon={menuIcon} color={blackColor} />
                <ActionSheet 
                    ref={(o) => this.settingsActionSheet = o}
                    options={this.props.language.settings.languagesOptions}
                    cancelButtonIndex={Settings.CancelButtonIndex}
                    onPress={this.onSettingsActionSheetPress.bind(this)}
                />
            </View>
        );
    }

    static CancelButtonIndex = 2;

    ...
}

And finally, you can start to create your components and bind the texts to specific parts of the language configuration. Let’s use our Feed component located in app/components/feed/index.js as an example, specifically the text we show when there are no images to display in the feed.

...

class Feed extends Component {

    ...

    render() {
        ...
        return (
            images.length ?
                ...
                :
                <View style={styles.textContainer}>
                    ...
                    <Text style={styles.centerText}>
                        {this.props.language.feed.noImagesToDisplay}
                    </Text>
                </View>
        );
    }

    ...

At the end, changing the language will look like this:

Loading the user-preferred language on every app start

The last thing we need to do is to check if the user uses a language different than the default. Remember: we persist the selected language id every time the user changes it. To achieve this we only need to read this setting, search for the set of translations and, once we have it, dispatch the action to set the language. This refreshes all our components showing localized texts. Check again our store in app/store.js:

...
const store = configureStore({});

//Load the user preferred language from local storage (or use default language if there is no language pre-selection)
//Once the language is loaded the store itself dispatch the action to update the language configuration
AsyncStorage.getItem(languageKeyName).then(languageKey => {
	const _languageKey = languageKey || config.defaultLanguageKey;
	const language = languagesConfig.find(l => l.languageKey === _languageKey);
	store.dispatch(appActions.setLanguage(language));
});

export default store;

Final thoughts

The benefits of being able to deliver real native experiences to our users are huge. The sensation of being “out” of the HTML5 paradigm, without totally quitting to javascript, is very refreshing.

I encourage you to start playing with React Native if you haven’t. I’m pretty sure it will be the next best way to create mobile apps, if it isn’t already. Now go start coding!

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM