Creating an Offline-First React Native App

I hated mobile development, but now I don’t, thanks to React Native

I started learning React Native in early October. Where did I start? Of course the documentation, specifically the Getting Started part. At that time, I wanted to learn as fast as I could, so I prefer the Quick Start tab (which uses Expo). Although it is extremely easy to setup with create-react-native-app (CRNA), we could see the downsides which are explained in the Caveats section.


In this part, I will introduce (or maybe re-introduce) you two cool packages made to speed up one’s learning of React and React Native.

Create React App

If you have used React before, you might be aware of create-react-app (CRA). It is a package intended for those who want to learn React swiftly. With CRA, all build tools (such as Webpack and its configs) are automatically configured so we don’t need to learn about them. It is not perfect, however. That’s why CRA has the script npm run eject, just in case we need to break out from CRA rigid environment and setup a new one without it. Overall, CRA will save you a lot of time if you are new to React and front-end web developments.

Create React Native App

CRNA was made to serve the same purpose as CRA. It handles all build configurations for Android and iOS, so again, we can save a lot of time. All applications created with CRNA are built in the cloud. From Expo CLI, we could run packager for local development with either a real device connected with an USB cable or a virtual device. If we want to build the APK/IPA for distribution, we could do so via the CLI too. The bundle size is pretty big compared to a normal APK. Imagine that the APK of a Hello World application has a size of ~27MB. That’s just too much. Luckily, CRNA has the same script as CRA does, npm run eject, which if executed, it will transform the current project directory to a new one — the same setup if you use react-native init. The eject script will also force you to get your hands dirty by configuring the build settings manually. At the end of the day, if you want to learn React Native real quick or build a demo app, I think CRNA is a good option. But I really discourage using CRNA for production builds. See also: Why Not Expo.


How It Began

I started with CRNA. I ejected because of the reasons explained in the “Why Not Expo” section. I was happy because I had freed myself from a shackle that would limit my application’s features. On the other side, when I first used Expo, I thought I didn’t have to deal with Android, Java, Gradle, and their friends (because I hated them). Turned out I should face them either way. But then again, I wasn’t alone because documentation came to the rescue. See the React Native’s documentation on Getting Started → Building Projects with Native Code tab. From this moment onward, I will only explain the Android part, because I don’t have Mac to build iOS (forgive me for being lazy to do the Hackintosh-thingy).

Before I learned about React Native, I had invested a lot of time creating web applications with ReactJS. Therefore, most of the time I used CSS classes to define the style of an element. That wasn’t the case when I first dived into React Native. Element styles are defined with inline CSS object and/or StyleSheet. When I define a CSS class, a hyphen is used for delimiter (eg. text-align), but in React Native, camelCase is used instead, so it becomes textAlign. It took me some time to adapt. The biggest difference is, if you don’t use StyleSheet, you should pass object to style property. But, if you use it, you should pass the property created by StyleSheet to the element. Here’s an example:

Based on the code above, both Text objects have the same font size. Then, what’s the real difference? When you use StyleSheet with { textStyle: { fontSize: 24 }} as a parameter, it will return an object, still with the key textStyle, but the object inside it is transformed to a number. I’m assuming this is how React Native emulates the CSS classes. Why should we pass a style object if we could pass a reference number to that object? However, this is not available in some cases. For example, if we use certain component libraries, we might want to be careful and read the props API carefully, because we don’t want to pass a number when the component props require an object.


Starting Packager Server for Development

Okay, first, we will create the Android application first, let’s say its name is SimpleRNApp. You will want to do what’s listed in React Native Getting Started, until the react-native run-androidpart. It’s up to you whether to use physical device or virtual device (such as Genymotion), but one of them is necessary to successfully run that script. We will touch the build part later, but for the time being, we will limit our scope to the development process. The screen should appear like this.

React-Native Getting Started First Interface

If you encounter white screen instead, press the application menu on your device, then choose Dev Settings. Then, click Debug server host & port for device. Finally, fill the input with the IP address that the packager server gave you before.

Running /home/imballinst/Android/Sdk/platform-tools/adb -s 192.168.56.101:5555 reverse tcp:8081 tcp:8081

The packager server tells me that it is running a server on the IP address and reversed port given above. So, the IP and port we must fill on the input is 192.168.56.101:8081. Is that it? No! Although the packager server outputs 192.168.56.101 (from the CLI), it doesn’t mean exactly that (I’m not sure if it’s just me or others too), because I can only access the packager server by changing the last 3 IP number to 1. So, instead of 192.168.56.101:8081, fill the input with 192.168.56.1:8081. If you are not certain about this, try opening that URL in a browser first. If the address is correct, the page should show: “React Native packager is running. Visit documentation”.


Application Flow

In this post, I will explain step-by-step how to create a simple, offline-first React-Native application. Below are the used technologies:

  1. View: react-native
  2. State Handling: redux, react-redux
  3. Offline Capabilities: redux-offline
  4. Routing: react-navigation

The screen structure in this tutorial is stack-based. There are two available components for the stack, Login and Drawer. Login is both a component and a screen, while Drawer is a thing that appears when you swipe a screen left-to-right or click a hamburger list. We exclude login screen from Drawer because it doesn’t make sense to have a drawer inside login screen. Inside the Drawer component, there are 3 screens, Home, Stub, and Detail, so in total we will create 4 screens.

  1. Login: a screen containing login form. If your credentials are valid, you will be redirected to the second screen, Home.
  2. Home: the first screen after a successful login. It contains 3 number objects that we can increment, decrement, or reset. Can navigate to Detail view by clicking “Detail View” button and Login view by clicking “Logout” button.
  3. Stub: can only be accessed from Drawer.
  4. Detail: can only be accessed from Home. Not accessible via Drawer.

Development and Testing Tools

I will skip the development and testing tools installation such as Jest and ESlint as they are not the core of this post. To see what development packages are used in this tutorial, head right to the package.json file inside the repository.


Project Structure

After knowing what we will develop, it is best if we can create the correct foundation for out application.

Project structure after react-native init SimpleRNApp

What we are going to do next is creating a source folder src. As I have written above that we will use Redux, let’s create the folders needed to separate the contexts clearly (you might want to use your own style of context separation). There are 5 folders we will create:

  1. actions: action creators (subfolder: __tests__)
  2. components: React components (subfolders: __tests__, helpers, assets, and modules)
  3. containers: for Redux-connected components (subfolder: none)
  4. routers: for routing (subfolders: native and routes), and
  5. store: for Redux stores and reducers (subfolders: helpers, middlewares, reducers)

Don’t forget to add __tests__ folder inside the store/reducers folder, because we will need it later to contain the unit tests. However, I won’t go into details about unit testing here. You can check the unit tests directly from the repository.


Redux

Let’s setup the Redux boilerplate. Execute this command to install the dependencies required by our store, npm install --save redux react-redux @redux-offline/redux-offline@^2.2.0 axios redux-thunk redux-persist@^4.5.0 redux-logger react-native-device-info react-navigation. We need redux-offline and redux-persist on these exact versions, because when this post was written, the newest version of redux-offline and redux-persist were incompatible to each other. Wait, what’s that react-native-device-info for? That’s a dependency to get our application version which is contained in android/app/build.gradle.

Actions

Firstly, create actions/auth.js which contains the actions needed to do a “login” — of course, we just do a “fake login” here with setTimeout.

import { NavigationActions } from 'react-navigation';
const TYPE_LOGIN_ATTEMPT = 'LOGIN_ATTEMPT';
const TYPE_LOGIN_SUCCESS = 'LOGIN_SUCCESS';
const TYPE_LOGIN_INVALID = 'LOGIN_INVALID';
const TYPE_LOGOUT_ATTEMPT = 'LOGOUT_ATTEMPT';
const TYPE_LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
const TYPE_LOGIN_REFRESH = 'LOGIN_REFRESH';
const refreshLoginView = () => ({ type: TYPE_LOGIN_REFRESH });
const login = (username, password) => (dispatch) => {
const promise = new Promise((resolve, reject) => {
dispatch({
type: TYPE_LOGIN_ATTEMPT,
username,
password,
});
    // Pretend that we're logging in within 0.25 second
setTimeout(() => {
if (password !== '') {
resolve({
type: TYPE_LOGIN_SUCCESS,
username,
});
} else {
reject();
}
}, 250);
});
  return promise.then(obj => dispatch(obj))
.then(() => dispatch(NavigationActions.navigate({ routeName: 'Home' })))
.catch(() => dispatch({
type: TYPE_LOGIN_INVALID,
message: 'Password is empty!',
}));
};
const logout = () => (dispatch) => {
const promise = new Promise((resolve) => {
dispatch({ type: TYPE_LOGOUT_ATTEMPT });
    // Pretend that we're logging out within 0.25 second
setTimeout(() => {
resolve({ type: TYPE_LOGOUT_SUCCESS });
}, 250);
});
  return promise.then(obj => dispatch(obj))
.then(() => dispatch(NavigationActions.navigate({ routeName: 'Login' })));
.then(() => setTimeout(() => dispatch(refreshLoginView()), 500));
};
export {
TYPE_LOGIN_ATTEMPT,
TYPE_LOGIN_INVALID,
TYPE_LOGIN_SUCCESS,
TYPE_LOGOUT_ATTEMPT,
TYPE_LOGOUT_SUCCESS,
TYPE_LOGIN_REFRESH,
login,
logout,
refreshLoginView,
};

The code snippet above is pretty much straight forward, with the exception of the dispatch parameter. We are able do that with the help of middleware redux-thunk, so we can dispatch an action inside an action. After that, let’s create actions/counter.js.

const actionCreator = (url, type, id, currentVal) => ({
type: `${type}_REQUEST`,
payload: { id, currentVal },
meta: {
offline: {
effect: {
method: 'HEAD',
url,
},
commit: {
type: `${type}_COMMIT`,
meta: { id },
},
rollback: {
type: `${type}_ROLLBACK`,
meta: { lastVal: currentVal, id },
},
},
},
});
const increment = (id, currentVal) => actionCreator(
'https://dog.ceo/api/breeds/list/all',
'INCREMENT',
id,
currentVal,
);
const decrement = (id, currentVal) => actionCreator(
'https://dog.ceo/api/breeds/list/all',
'DECREMENT',
id,
currentVal,
);
const reset = (id, currentVal) => actionCreator(
'https://dog.ceo/api/breeds/list/all',
'RESET',
id,
currentVal,
);
export {
actionCreator,
increment,
decrement,
reset,
};

Since we are going to create an offline-first application, of course we should involve network connection. The actionCreatorfunction receives 4 parameters, url, type, id, and currentVal. We don’t really need the response body, so HEAD method to a public API should be enough. The second parameter, type, will be concatenated for the action type, while id and currentVal are used to determine which data will be updated. Because we are utilizing the device’s network, you will need to add this to your app/src/main/AndroidManifest.xml for the permission. Otherwise, an error will be thrown when the application runs.

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Store and Reducers

Let’s start from the sub-folders first, store/helpers. Inside that folder, create AppVersion.js.

import DeviceInfo from 'react-native-device-info';
const appVersion = DeviceInfo.getVersion();
export default appVersion;

Now, whenever we import AppVersion.js, the imported value is our application version. And then, remember that we can use dispatch inside our actions? We will need to create store/middlewares/index.js and export an array containing redux-thunk.

import ReduxThunk from 'redux-thunk'
export default [ReduxThunk];

Guess what’s missing in our store? Of course, the reducers! There are three reducers, nav, auth, and counter. Let’s start from store/reducers/nav.js.

import { NavigationActions } from 'react-navigation';
import { AppNavigator } from '../../routers';
const { getStateForAction } = AppNavigator.router;
const { init, NAVIGATE } = NavigationActions;
const initialNavState = getStateForAction(init());
const nav = (state = initialNavState, action) => {
if (action.type === NAVIGATE) {
let isDrawer = false;
    const firstIndexOfView = state.routes.findIndex((route) => {
const { routeName, routes } = route;
      if (routes) {
// Drawer has route.routes, which contains DrawerClose, DrawerOpen, DrawerToggle
// The DrawerClose object has index and routes, indicating the current Drawer view
isDrawer = true;
        return true;
}
return action.routeName === routeName;
});
    if (firstIndexOfView === -1 || isDrawer) {
return getStateForAction(action, state);
}
    const newIndex = firstIndexOfView;
const newRoutes = [...state.routes].splice(0, newIndex + 1);
const newNav = { index: newIndex, routes: newRoutes };
    return newNav;
}
  return state;
};
export default nav;

We create this reducer because we want to integrate Redux with React Navigation (you can see it here). We initialized the default state for the navigation with getStateForAction(init()). Then, we create our reducer function which receives two parameters, the state and the action. If the action is NAVIGATE, it will return the new state. But, there’s more to it. Remember that we will use the Stack concept? It means, for every screen change from Login to Drawer and back, there will be 3 routes pushed instead of 2. The condition block is meant to prevent that from happening.

Let’s take our application as an example which has 4 screens: Login, Home, Stub, and Detail, with the last 3 is contained within a Drawer. Then, the Login and Drawer are encapsulated within a Stack. If we go from Login-Home-Login-Home-Login-Home, the stack would look like this: [Login,Home,Login,Home,Login,Home]. What we want is [Login,Home] only. Why? Because we are using redux-persist to rehydrate our store. If we don’t flatten the stack, when we re-open our application, we will see an absurd flash of screen changes. Don’t forget to export the function and we go to the next one, store/reducers/auth.js.

import {
TYPE_LOGIN_ATTEMPT,
TYPE_LOGIN_INVALID,
TYPE_LOGIN_SUCCESS,
TYPE_LOGOUT_SUCCESS,
TYPE_LOGIN_REFRESH,
} from '../../actions/auth';
const defaultState = {
isLoggedIn: false,
username: '',
isError: false,
message: '',
};
const auth = (state = defaultState, action) => {
const { type, username = '', message = '' } = action;
switch (type) {
case TYPE_LOGIN_ATTEMPT: {
return {
...state,
message: 'Logging in...',
isError: false,
};
}
case TYPE_LOGIN_SUCCESS: {
return {
...state,
isLoggedIn: true,
username,
message,
isError: false,
};
}
case TYPE_LOGIN_INVALID: {
return {
...state,
message,
isError: true,
};
}
case TYPE_LOGOUT_SUCCESS: {
return {
...state,
isLoggedIn: false,
};
}
case TYPE_LOGIN_REFRESH: {
return {
...state,
message,
username,
isError: false,
};
}
default: return state;
}
};
export { defaultState };
export default auth;

This is the authentication reducer, which contains the login state and the username logged in. Pretty much straight forward. The last one, store/reducers/counter.js, is the most interesting for me because it uses redux-offlineaction types.

const defaultState = [
{ id: 0, val: 1, sync: true },
{ id: 1, val: 2, sync: true },
{ id: 2, val: 3, sync: true },
];
// Helper functions
const modifyVal = (state, id, newVal, newStatus) => {
const stateObject = state.find(obj => obj.id === id);
  return state.slice(0, id)
.concat({ ...stateObject, val: newVal, sync: newStatus })
.concat(state.slice(id + 1, state.length));
};
const modifySync = (state, id, newStatus) => {
const stateObject = state.find(obj => obj.id === id);
  return state.slice(0, id)
.concat({ ...stateObject, sync: newStatus })
.concat(state.slice(id + 1, state.length));
};
// Export the helper functions for testing and the reducer
export { modifyVal, modifySync };
export default (state = defaultState, action) => {
const { type, meta, payload } = action;
const { id, currentVal } = payload || {};
  switch (type) {
case 'INCREMENT_REQUEST':
return modifyVal(state, id, currentVal + 1, false);
case 'DECREMENT_REQUEST':
return modifyVal(state, id, currentVal - 1, false);
case 'RESET_REQUEST':
return modifyVal(state, id, 0, false);
case 'INCREMENT_COMMIT':
case 'DECREMENT_COMMIT':
case 'RESET_COMMIT':
return modifySync(state, meta.id, true);
case 'INCREMENT_ROLLBACK':
case 'DECREMENT_ROLLBACK':
case 'RESET_ROLLBACK':
return modifyVal(state, id, meta.lastVal, true);
default:
return state;
}
};

This one is pretty complicated at first. The idea is, we want to create an offline-first application and therefore, we should make the application responsive, even if we don’t have internet connection. I use the pattern REQUEST, COMMIT, and ROLLBACK. The first one, REQUEST, is an action type to label that the action has been dispatched, including the HTTP request. We immediately update the reducer regardless of the response of the HTTP request (without redux-offline, we will need to wait the response first before we update the reducer). The second one, COMMIT, is the name of dispatched action type if the HTTP request is successful. We can use the meta property (which we have defined in the actions before) to provide information about which element should be updated. The last one, ROLLBACK, is the counterpart of COMMIT, which will be dispatched if the HTTP request is failed (or timed out). Lastly, create store/reducers/index.js as an entry point for the folder.

import { combineReducers } from 'redux';
import nav from './nav';
import counter from './counter';
import auth from './auth';
export default combineReducers({ nav, counter, auth });

Import combineReducers from redux package and import all the other reducers. Export the combined reducers and you are good to go. Let’s head to the final part of Redux, the store! Create file store/index.js.

import { applyMiddleware, createStore, compose } from 'redux';
import { offline } from '@redux-offline/redux-offline';
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
import axios from 'axios';
import logger from 'redux-logger';
import { persistStore } from 'redux-persist';
import { AsyncStorage } from 'react-native';
import getAppVersion from './helpers/AppVersion';
// Middlewares
import middlewares from './middlewares';
// Reducers
import combinedReducers from './reducers';
const persistStorage = (store, options, callback) => {
AsyncStorage.getItem('reduxPersist:appVersion')
.then((itemValue) => {
const getPersistedStore = () => persistStore(
store,
{ storage: AsyncStorage, ...options },
callback,
);
const currentAppVersion = getAppVersion;

if (itemValue) {
// If version is identified
const app = JSON.parse(itemValue);
        if (app && app.version !== currentAppVersion) {
getPersistedStore().purge();
          AsyncStorage.setItem(
'reduxPersist:appVersion',
JSON.stringify({ version: currentAppVersion }),
);
} else {
getPersistedStore(); // .purge to clean the offline data
}
} else {
// If no, define one
AsyncStorage.setItem(
'reduxPersist:appVersion',
JSON.stringify({ version: currentAppVersion }),
);
}
});
};
const reduxOfflineConfig = {
...offlineConfig,
persist: persistStorage,
effect: effect => axios(effect),
discard: (error, action, retries) => {
const { response } = error;
    return (response && response.status >= 400) || retries > 10;
},
};
const myMiddlewares = [...middlewares];
if (__DEV__) {
myMiddlewares.push(logger);
}
const store = createStore(
combinedReducers,
compose(
applyMiddleware(...myMiddlewares),
offline(reduxOfflineConfig),
),
);
export default store;

Okay, so what the heck did we write?

First, we create persistStorage function which basically gets the property reduxPersist:appVersion then checks whether a value exists or not. If it does, it will check whether the current version and the stored version is same or not. If the version is same, it will rehydrate the store, if not, the old store will be purged. In any case, if the version is missing or mismatches the one from the store, a new one will be set. For me, it is going to be useful to handle application updates that change the store structure. For example, let’s say that our authentication state in v0.0.1 is only a boolean, indicating the user’s login status. Then, we release an update which changes it [the authentication state] to an object, containing boolean and string. The application will have incompatibility issues when it runs because it rehydrates a string to what-supposed-to-be an object.

Back to the code after the persistStorage, we set up our redux-offline config and middlewares. Use them during store creation, and we are done for this segment!


Components

First things first, execute npm install --save prop-types so we can do PropTypes checking. After that, let’s begin creating our components! But before that, let me warn you first, if you are wondering later where the prop navigation comes from, it’s from react-navigation’s addNavigationHelpers which we will uncover later.

Create a file in src/components named Login.js. Also, add a PNG picture to src/components/assets, give it the name loginpic.png. It will serve as the header logo in our code below.

import React from 'react';
import PropTypes from 'prop-types';
import { View, Button, Text, TextInput, StyleSheet, Image } from 'react-native';
import loginPic from './assets/loginpic.png';
const style = StyleSheet.create({
viewStyle: {
backgroundColor: '#c1c3c8',
flex: 1,
paddingHorizontal: '16.5%',
height: '100%',
},
loginTitle: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 40,
},
loginImage: {
opacity: 1,
width: 200,
height: 100,
resizeMode: 'contain',
},
loginImageContainer: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center',
height: 200,
},
formContainer: {
flex: 2,
},
textInput: {
padding: 5,
marginBottom: 15,
alignItems: 'stretch',
},
textProcessMessage: {
color: '#0000ff',
},
textErrorMessage: {
color: '#ff0000',
},
});
class Login extends React.Component {
state = { username: '', password: '' }
  componentDidMount() {
this.props.refreshLoginView();
this.setState({
username: '',
password: '',
});
}
  componentWillReceiveProps(nextProps) {
if (this.state.username !== '' && nextProps.username === '') {
this.setState({
username: '',
password: '',
})
}
}
  onChange = type => (text) => {
this.setState({
[`${type}`]: text,
});
}
  onPress = () => {
const { login } = this.props;
const { username, password } = this.state;
    login(username, password);
}
  render() {
const { message, isError } = this.props;
const textMessageStyle = isError ? style.textErrorMessage : style.textProcessMessage;
return (
<View style={style.viewStyle}>
<View style={style.loginImageContainer}>
<Image source={loginPic} style={style.loginImage} />
</View>
<View style={style.formContainer}>
<Text style={style.loginTitle}>Login</Text>
<TextInput
placeholder="Username"
style={style.textInput}
value={this.state.username}
onChangeText={(this.onChange('username'))}
/>
<TextInput
placeholder="Password"
style={style.textInput}
value={this.state.password}
onChangeText={this.onChange('password')}
secureTextEntry
/>
<Text style={textMessageStyle}>{message}</Text>
<Button
color="#588bd4"
onPress={this.onPress}
title="Login"
/>
</View>
</View>
);
}
}
Login.propTypes = {
isError: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
refreshLoginView: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
};
export default Login;

If you are familiar with React, you shouldn’t have problems understanding React-Native. More or less the same, but as I have written above, one of the major differences is how we style our components. In RN, we either use inline CSS or StyleSheet object. You can see on the code above that all components use the StyleSheet object except for Button, because Button component from RN isn’t really customizable. If you want a customizable one, check out RN component libraries, such as NativeBase or react-native-elements. Regarding styling, take a look on the StyleSheet object, we have a lot of flexbox-thingy. I recommend reading A Guide to Flexbox to gain better comprehension of it.

We still need to create another 3 components. To prettify things, inside the Homecomponent, we will use NumberButtons for better modularization. Create the file components/modules/NumberButtons.js.

import React from 'react';
import PropTypes from 'prop-types';
import { View, Button, Text, StyleSheet } from 'react-native';
const style = StyleSheet.create({
row: {
flexDirection: 'row',
},
flex1: {
flex: 1,
},
textCenter: {
textAlign: 'center',
},
});
const NumberButtons = ({
id, num, sync, onModifyCounter,
}) => (
<View>
<View>
<Text style={style.textCenter}>{num} | {sync ? 'Sync' : 'Not sync'}</Text>
</View>
<View style={style.row}>
<View style={style.flex1}>
<Button onPress={onModifyCounter(id, 'increment', num)} title="Increment" />
</View>
<View style={style.flex1}>
<Button color="#00aa00" onPress={onModifyCounter(id, 'decrement', num)} title="Decrement" />
</View>
<View style={style.flex1}>
<Button color="#ff0000" onPress={onModifyCounter(id, 'reset', num)} title="Reset" />
</View>
</View>
</View>
);
NumberButtons.propTypes = {
id: PropTypes.number.isRequired,
num: PropTypes.number.isRequired,
sync: PropTypes.bool.isRequired,
onModifyCounter: PropTypes.func.isRequired,
};
export default NumberButtons;

The component module above contains 5 key elements, counter, sync status, and the 3 buttons. We will use it on components/Home.js.

import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, Button, Text } from 'react-native';
import NumberButtons from './modules/NumberButtons';
const style = StyleSheet.create({
viewStyle: {
backgroundColor: '#c1c3c8',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
paddingHorizontal: '16.5%',
height: '100%',
},
titleviewStyle: {
marginBottom: 30,
},
titleTextStyle: {
fontSize: 24,
},
});
class Home extends React.Component {
onModifyCounter = (id, type, currentVal) => () => {
const {
increment,
decrement,
reset,
} = this.props;
const dispatcher = {
increment,
decrement,
reset,
};
    dispatcher[type](id, currentVal);
}
  onPressDetailView = () => {
this.props.navigation.navigate('Detail');
}
  onLogout = () => {
this.props.logout();
}
  render() {
const { counter, username } = this.props;
const groupNumberButtons = counter.map(({ id, val, sync }) => (
<NumberButtons
key={`numbtns-${id + 1}`}
id={id}
num={val}
sync={sync}
onModifyCounter={this.onModifyCounter}
/>
));
    return (
<View style={style.viewStyle}>
<View style={style.titleViewStyle}>
<Text style={style.titleTextStyle}>Hi, {username}!</Text>
</View>
        {groupNumberButtons}
        <View style={{ marginTop: 30 }}>
<Button onPress={this.onPressDetailView} title="Detail View" />
</View>
        <View style={{ marginTop: 30 }}>
<Button color="#ff0000" onPress={this.onLogout} title="Logout" />
</View>
</View>
);
}
}
Home.propTypes = {
navigation: PropTypes.object.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
counter: PropTypes.array.isRequired,
username: PropTypes.string.isRequired,
};
export default Home;

The most complex thing in the two code snippets above is how we style the screen. Basicly, we want to create 3 rows of element groups, with a number displayed above the 3 buttons, centered. Here is the interesting part. In onModifyCounter event handler, we destructured the props, getting counter, and the 3 functions (increment, decrement, reset). According to the action functions we have created before, they all have the same parameters, so I made an object instead and call the respective function based on the type (this too to prevent ESlint error no-unused-props).

Let’s create the next one. Just like its name, this one doesn’t contain anything since its job is only to enable changing screens via drawer: components/Stub.js.

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const style = StyleSheet.create({
viewStyle: {
backgroundColor: '#c1c3c8',
flex: 1,
paddingHorizontal: '16.5%',
height: '100%',
},
});
const Stub = () => (
<View style={style.viewStyle}>
<Text>This is Stub view, accessible only via Drawer</Text>
</View>
);
Stub.propTypes = {};
export default Stub;

Alright, to the last one, just like stub, the intention of this last screen is to show the possibility of a screen being not included in the drawer (but still accessible). Create components/Detail.js.

import React from 'react';
import PropTypes from 'prop-types';
import { View, Button, Text, StyleSheet } from 'react-native';
const style = StyleSheet.create({
viewStyle: {
backgroundColor: '#c1c3c8',
flex: 1,
paddingHorizontal: '16.5%',
height: '100%',
},
});
class Detail extends React.Component {
onPressBack = () => {
this.props.navigation.navigate('Home');
}
  render() {
return (
<View style={style.viewStyle}>
<Text>This is Detail View, but it doesn&apos;t appear in Drawer</Text>
        <Button onPress={this.onPressBack} title="Back" />
</View>
);
}
}
Detail.propTypes = {
navigation: PropTypes.object.isRequired,
};
export default Detail;

Okay, that’s all 4 components, done! Later, when we have done setting up react-navigation in our project, we will open the files above again, because we need to adjust our components and event handlers.


Container Components

Container components is a component connected to Redux store. Since Detail and Stub doesn’t have correlation to the store whatsoever, only Login and Home views will have their components connected to the store. Let’s start with containers/Login.js:

import { connect } from 'react-redux';
import { login, refreshLoginView } from '../actions/auth';
import Login from '../components/Login';
const mapStateToProps = ({ auth }) => {
const { message, isError, username } = auth;
  return {
isError,
message,
username,
};
};
const mapDispatchToProps = { login, refreshLoginView };
export default connect(mapStateToProps, mapDispatchToProps)(Login);

In our Login screen, as written in Login.propTypes before, we need to pass down the auth state, login action, and refreshLoginView action from the store as props. It’s the same for Home screen, as it has lots of props to pass down as well. Get into it by creating containers/Home.js.

import { connect } from 'react-redux';
import { logout } from '../actions/auth';
import { increment, decrement, reset } from '../actions/counter';
import Home from '../components/Home';
const mapStateToProps = ({ counter, auth }) => ({
counter,
username: auth.username,
});
const mapDispatchToProps = {
logout, increment, decrement, reset,
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);

The Home screen should be able to increment, decrement, and reset the objects counter, so we need to pass the actions. Not to forget, we want to say “Hi” to the username and retain the modified counter state!

We are done for the containers! Now, onto the final stretch here, the routers — using react-navigation!


Routers

Okay, now, we need to install react-native-vector-icons so we can use cool icons! That’s a lie, actually. We are just gonna use it for the hamburger list icon: npm install --save react-native-vector-icons.

Let’s create our very first hamburger-list icon (yay!): routers/native/modules/NavbarItem.js.

import React from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
const NavbarItem = ({ iconName, onPress }) => (
<TouchableOpacity
style={{ paddingHorizontal: 20 }}
onPress={onPress}
>
<Icon name={iconName} size={20} color="#fff" />
</TouchableOpacity>
);
NavbarItem.propTypes = {
iconName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
};
export default NavbarItem;

Here, we use our fresh react-native-vector-icons to create font-awesome icons. The onPress and iconName prop will be passed down later from its parent component, which is routers/native/modules/DrawerMenu.js.

import React from 'react';
import PropTypes from 'prop-types';
import NavbarItem from './NavbarItem';
class DrawerMenu extends React.Component {
onDrawerNavigate = () => {
const { navigation } = this.props;
const drawerAction = navigation.state.index === 0 ? 'DrawerOpen' : 'DrawerClose';
    navigation.navigate(drawerAction);
}
render() {
return (
<NavbarItem
iconName="bars"
onPress={this.onDrawerNavigate}
/>
);
}
}
DrawerMenu.propTypes = {
navigation: PropTypes.object.isRequired,
};
export default DrawerMenu;

See, the important part here is navigation.state.index. It represents the state of the drawer, whether it is open or not. Zero means it is closed, otherwise it is open. We just need to juggle it on our hamburger-list press. Next, we will create an utility file for the Drawer: routers/native/utils/NavUtil.js.

const getNavigationOptionsWithAction = (title, backgroundColor, color, headerLeft) => ({
title,
headerStyle: {
backgroundColor,
},
headerTitleStyle: {
color,
},
headerTintColor: color,
headerLeft,
});
const getDrawerNavigationOptions = (title, backgroundColor, titleColor, drawerIcon) => ({
title,
headerTitle: title,
headerStyle: {
backgroundColor,
},
headerTitleStyle: {
color: titleColor,
},
headerTintColor: titleColor,
drawerLabel: title,
drawerIcon,
});
export {
getNavigationOptionsWithAction,
getDrawerNavigationOptions,
};

Okay so, it is hard to see what are these functions for at first, but they will come in handy to generate our Drawer content/menu options. Let’s see them in action: routers/native/Drawer.js.

import React from 'react';
import Icon from 'react-native-vector-icons/FontAwesome';
import { DrawerNavigator } from 'react-navigation';
import Home from '../../containers/Home';
import Detail from '../../components/Detail';
import Stub from '../../components/Stub';
import { getNavigationOptionsWithAction, getDrawerNavigationOptions } from './utils/NavUtil';
import DrawerMenu from './modules/DrawerMenu';
const getDrawerIcon = (iconName, tintColor) => (
<Icon name={iconName} size={20} color={tintColor} />
);
const homeDrawerIcon = ({ tintColor }) => getDrawerIcon('home', tintColor);
const stubDrawerIcon = ({ tintColor }) => getDrawerIcon('table', tintColor);
const homeNavOptions = getDrawerNavigationOptions('Home', '#26ba9a', 'white', homeDrawerIcon);
const stubNavOptions = getDrawerNavigationOptions('Stub', '#26ba9a', 'white', stubDrawerIcon);
const detailNavOptions = getDrawerNavigationOptions('Detail', '#26ba9a', 'white', undefined);
const Drawer = DrawerNavigator({
Home: {
screen: Home,
navigationOptions: homeNavOptions,
},
Stub: { screen: Stub, navigationOptions: stubNavOptions },
Detail: {
screen: Detail,
navigationOptions: { ...detailNavOptions, drawerLabel: () => null },
},
}, {
drawerWidth: 300,
drawerPosition: 'left',
initialRouteName: 'Home',
});
Drawer.navigationOptions = ({ navigation }) => getNavigationOptionsWithAction(
'TestDrawer',
'#26ba9a',
'#000',
<DrawerMenu navigation={navigation} />,
);
export default Drawer;

Let’s start from the top. First, we create a helper getDrawerIcon function which will return an icon. That icon will be side-by-side with the menu text inside the drawer content. Next, we will create navigation option for views inside the Drawer — in this case, Home, Stub, and Detail — with getDrawerNavigationOptions. We won’t display Detail in the drawer content, so it’s safe to pass undefined to the 4th function parameter. Lastly, we use all of them as an object in the first parameter of DrawerNavigator constructor, with each having screen and navigationOptions property. Of course, again, since Detail won’t appear inside the drawer content, we should return set the drawerLabel as a function that returns null.

The second parameter of the constructor is the general options of the drawer, such as width, position, and initial route. Finally, we modify the Drawer’s navigation options so when we click the hamburger-list, the Drawer’s state will change, along with the header title.

The Drawer is done, now for the finishing touch, we create our stack with StackNavigator. Create routers/index.js .

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { BackHandler } from 'react-native';
import { NavigationActions, StackNavigator, addNavigationHelpers } from 'react-navigation';
import Login from '../containers/Login';
import Drawer from './native/Drawer';
const AppNavigator = StackNavigator({
Login: { screen: Login, navigationOptions: { header: null } },
Drawer: { screen: Drawer },
});
class AppWithNavigationState extends React.Component {
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.onBackPress);
}
  componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.onBackPress);
}
  onBackPress = () => {
const { dispatch, nav } = this.props;
    if (nav.index === 0) {
return false;
}
    const screenKeyBefore = nav.routes[nav.routes.length - 2].routeName;
dispatch(NavigationActions.navigate({ routeName: screenKeyBefore }));
    return true;
}
  render() {
const { dispatch, nav } = this.props;
    return (
<AppNavigator
navigation={addNavigationHelpers({ dispatch, state: nav })}
/>
);
}
}
AppWithNavigationState.propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
const mapStateToProps = ({ nav }) => ({ nav });
export { AppNavigator };
export default connect(mapStateToProps)(AppWithNavigationState);

We initialized our StackNavigator with the Login screen and Drawer we have made. Then, we create a class that will handle back button press (in Android). The most interesting part is the render method. We get dispatch and nav from the props and we pass it to AppNavigator inside navigation prop. This is the thing that appeared a lot in our components! With the help of addNavigationHelpers, every screen change will affect the store as well. We export two things, one is AppNavigator (so it could be used in store/reducers/nav.js), the other one is the connected AppNavigator with store.


Final Touch

Do react-native link to link Android and Node dependencies. This is needed for react-native-device-info and react-native-vector-icons package. After that, create an index file src/index.js containing this snippet.

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import AppNavigatorWithStore from './routers';

const App = () => (
<Provider store={store}>
<AppNavigatorWithStore />
</Provider>
);

export default App;

Finally, delete /App.js because we don’t use it anymore and change the contents of /index.js to import our newly created src/index.js. Now, our root index will point to the whole application we just made.

import { AppRegistry } from 'react-native';                       import App from './src';
AppRegistry.registerComponent('SimpleRNApp', () => App);

I Want to See the Result!

Okay, okay! The first one is the Login screen. Here, as we have coded before, we should enter our username and password. Of course, no real authentication whatsoever but we keep a little validation in the view by preventing an empty password field.

The password was empty, so I entered a not-empty one

To create the same state as the above screen, submit an empty password. Next, we will want to log in by filling the password and clicking the “Login” button. Your screen will change from the Login to Home screen!

Home Screen

On the top-side of Home screen, you can see that we have our username getting a greeting from the application. After that, we have 3 sets of NumberButtons which we can play around with. Try clicking the buttons and see the number and text above the button groups are changing. The “sync” and “not sync” word is determined by how fast our connection is. If we click those buttons when we are offline, it will stay at “not sync” until we are connected to the internet. Up next, the Stub screen! But, to navigate to Stub screen, we need to open our Drawer first. Swipe left-to-right, or click the hamburger list on the top left.

Post-login Drawer

Now that we have opened our Drawer, we can navigate to the Stub screen.

Stub screen

The Stub screen doesn’t have that much feature like Home screen does. It is just purely for navigation purposes. Next, we will navigate to Detail screen by going back to the Home Screen (again, via the drawer). Then, click the “Detail View” button.

The Detail screen, not much, really

This is just the same for the Stub screen, but the difference is this screen doesn’t appear in the Drawer menu. See, our Drawer menu only contains two menus, Home and Stub. Now, we will test the store rehydration by closing the application, then open it again. It will restore the previous state (instead of starting over). That’s the work of redux-persist. The last one, check our back button handler! Normally, if we use the back button from the Home screen, it will close our application, right? With the handler we have made before, the screen will change to the Login screen instead. because StackNavigator contains our previous stack which was [Login, Home]. Well, that’s it! Our application is done, yay!


Building the APK

Now that we have our application on debug mode done, it’s time to build the APK so we can distribute it! This link will help you throughout the process: Generating Signed APK. It’s pretty easy!


References

Thanks to all these sources, I now have better understanding this whole thing:

  1. Expo Docs, helped me to “walk in the world of mobile” for the first time. Without it, I probably wouldn’t have written this post
  2. React-Native Docs, because why not
  3. React-Navigation Docs, also because why not
  4. Github user kyaroru for her wonderful repository which gave me a lot of insight and examples about redux integration with react-navigation
  5. A Complete Guide to Flexbox and Flexbox Defense