Klyp have long been involved with mobile app development, building not only native Android and iOS apps with Java and Objective-C, but also building hybrid mobile apps using AngularJS, Ionic and Cordova.
But everyone knows that native is the holy grail when it comes to app development. It ensures maximum device compatibility, and optimal performance. And who wouldn't want that? Well, unfortunately, sometimes our clients' budgets don't always stretch to being able to build two apps, each with its own, entirely separate, code base.
Previously, when such a predicament had arisen, we've opted to build a hybrid mobile app, but only when the end result wouldn't be adversely compromised by such a tech stack. That was, until we decided to invest some time scoping out React Native.
What is React Native?
React Native is a framework developed by Facebook, which allows you to build a native mobile app written entirely in JavaScript. Well, almost. And by using the same fundamental UI elements used to develop Android and iOS apps, React Native apps are indistinguishable from apps written with Java and Objective-C.
So, what happened next?
Well we loved what we learned, and decided that we had to put it to the test. Which brings me neatly onto the topic of this post. I was tasked with building our second React Native mobile app and so thought I'd share some of the valuable lessons I've learned during my first ever mobile app project.
1. Use Redux
I had heard about Redux before I started working with React Native, but I hadn't used it before, nor did I really know what it could do for me.
In a nutshell, Redux is a “predictable state container”, which consolidates your app's state into a centralised data store - your source of truth. This data can then be manipulated through the use of actions, which are payloads to send data to the store; and reducers, which define how the store should update as a result of these data payloads.
But why use Redux, you might ask?
Well, Redux took the headache out of managing the data and state of my application.
Rather than having to store all of the data in one or more high level components, and pass it down to child components by way of props and back using callbacks, I could instead bind the Redux state straight into high level scene containers as and when required, and then use actions to invoke reducers whenever I needed to change my data.
But that's not to say that I didn't still use component level state to persist data during run-time. Ultimately though, everything came from and went back to the Redux store.
As for the learning curve to understanding and implementing Redux? I found the learning curve to be pretty shallow, but that said, I did find myself having to read through the examples a few of times before I was sure that I understood what was happening and how to apply the principles to my own app.
One gotcha I found is that your reducer name denotes the key under which your data is stored in your store. Therefore, in the example below, the counter property on the state object would be incremented and decremented.
import createReducer from 'redux-createreducer';
import {
COUNTER_DECREMENT,
COUNTER_INCREMENT,
} from '../config/constants';
export const counter = createReducer(0, {
COUNTER_INCREMENT: (state, action) =>; {
return state + 1;
},
COUNTER_DECREMENT: (state, action) =>; {
return state - 1;
},
});
// Initial state
{
counter: 0,
}
2. Use a Redux storage engine
Following on from saying to use Redux, if you choose to, I would highly recommend that you use a Redux storage engine too. Doing so will allow you to seamlessly load and save your application data from a mobile device.
Why is this important?
Well, it's just one less thing to have to worry about. As soon as your app is opened, your data is immediately restored to your store, and every time you modify your store using a reducer, the data is immediately written to your chosen storage medium. Both of these actions can be configured, but this is the default.
There are a number of different storage engines to choose from, and I opted for the React Native Async Storage engine. Here's a snippet showing how I set up my Redux store and configured it to use async storage.
import {
createStore,
applyMiddleware,
} from 'redux';
import createLogger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import {
reducer as applyStorageReducer,
createLoader as createStorageLoader,
createMiddleware as createStorageMiddleware,
} from 'redux-storage';
import createEngine from 'redux-storage-engine-reactnativeasyncstorage';
import reducers from '../reducers';
import { REDUX_STORAGE_KEY } from '../config/constants';
// Apply storage reducer to listen for LOAD action into reducers
const rootReducer = applyStorageReducer(reducers);
// Create storage engine, loader and middleware
const engine = createEngine(REDUX_STORAGE_KEY);
const load = createStorageLoader(engine);
const storageMiddleware = createStorageMiddleware(engine);
// Create logger middleware
const loggerMiddleware = createLogger({ predicate: () => __DEV__ });
// Apply store middleware
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, storageMiddleware, loggerMiddleware)(createStore);
// Create and load store
const store = createStoreWithMiddleware(rootReducer);
load(store);
export default store;
3. Create an image map
In order to include an image in your React Native app, you need to use the require
function. One limitation I encountered when using require
was that you cannot use variables in the file path argument.
{/* This will work */}
{/* This will not work */}
const filename = ‘image';
One excellent workaround I found for this (but cannot recall the source) was to create an image map.
export default {
logo: {
primary: require('./img/logo-primary.png'),
secondary: require('./img/logo-secondary.png'),
},
};
Using this approach, rather than calling require
directly, you're instead getting the value of an object property, and thus can build the path to that property however you wish.
import images from ‘./assets/images';
{/* This will work */}
{/* And this will work too */}
const logo = ‘secondary';
An image map also offers you the added benefit of having all of your images defined in a single location. So if you need to modify an image filename, or move images to a different location, you won't need to trawl through your entire app to find and replace each and every reference.
4. Viewport units
At the time of writing this post, React Native doesn't support percentage values when defining styles.
Now this isn't too much of an issue given that React Native does support the flexbox layout, so achieving columnar layouts isn't too hard. That being said however, I did still encounter situations where I needed a percentage based dimension.
Fortunately, someone else in the React Native community had the same problem and came up with a tiny viewport units library. The basic premise is that you divide the viewport height and width by 100 to get a single percentage point, and provide a helper function to scale your value accordingly.
import { Dimensions } from 'react-native';
// Prepare window height and width
const { height, width } = Dimensions.get('window');
export function vh(value) {
return value * height / 100;
}
export function vw(value) {
return value * width / 100;
}
// 50% and 25% width
const half = vw(50);
const quarter = vw(25);
Obviously this only works with the viewport dimension, but the premise can be applied, with some modifications, to other components as well.
5. Don't forget componentWillReceiveProps
During the development of my own components, I found that they would not always update to reflect changes in props as I would have expected.
I later discovered that this was due to a really obvious oversight on my part, but I thought that hey, I missed it, so other people might overlook it too.
Here's a really simple example of what I was essentially doing:
import React from 'react';
import { Text } from 'react-native';
class Square extends React.Component {
static propTypes = {
value: React.PropTypes.number,
};
constructor(props) {
super(props);
this.value = Math.pow(props.value, 2);
}
render() {
return (
{this.props.value} x {this.props.value} = {this.value}
);
}
}
export default Square;
As you can see, I was using the value
prop to set the state
in the constructor. This meant that when a new prop value was supplied from a parent component, the value of this.state.value
wouldn't be recalculated, because it was calculated and set in the constructor, which is only called during instantiation.
The solution (which I acknowledge is an over complication for this example) is to use the componentWillReceiveProps
lifecycle method. This method will receive the new prop values and allow you to update your component's state accordingly.
import React from 'react';
import { Text } from 'react-native';
class Square extends React.Component {
static propTypes = {
value: React.PropTypes.number,
};
constructor(props) {
super(props);
this.state = {
value = Math.pow(props.value, 2),
};
}
componentWillReceiveProps(nextProps) {
this.setState({
value: Math.pow(nextProps, 2),
});
}
render() {
return (
{this.props.value} x {this.props.value} = {this.state.value}
);
}
}
export default Square;
Thanks for reading. I hope that at least one of these lessons will save you some time or give you food for thought for your own mobile app.
If you have any go-to code snippets that you use across your apps, or you have any questions or comments about this post, please do give me a shout. I'm always keen to learn and to help others do the same, hit me up at engage@klyp.co!