Adventures with React Native
There And Back Again
A couple weeks ago I was at a coffee shop waiting for my order and getting my morning dose of Twitter while I waited. As I scrolled through my feed on my iPhone, I noticed several of my friends were posting weird JavaScript behavior using the #wtfjs hashtag. There was some pretty classic JS weirdness like [2, 12].sort()
, and 0.1 + 0.2
(go ahead and try them out in your console -- I'll wait).
As I was reading these I wanted to try them out myself. But alas I had nothing but my phone at my disposal, which in spite of countless apps in the App Store, lacks any type of JavaScript REPL.
Spolier alert: There is now a JS REPL for your phone, PocketNode. The rest of this post is the why and how of it.
As I sat there waiting for my breakfast, I lamented the abundant lack of REPL on my phone. I pondered why all my fellow programmers hadn't anticipated my needs this particular morning, and built me an app, it occurred to me "I am a programmer, I could make an app".
Nearly my entire career has been web development. I've built some VB apps and done a bit of C/C++ early in my career, but that was a long time ago and I have never even tried to learn Objective C. It seemed that building a mobile JavaScript REPL was way outside my wheel house. I would need to learn Objective C, which would take me weeks (months?). Then figure out how to evaluate the JavaScript input, which would mean either figuring out how to embed an existing run time a la V8 (yuck), writing my own runtime (double yuck), or looking at how React Native was utilizing a JavaScript thread to process logic, then render natively. On top of all that, I would still have to write the actual iOS application and then repeat the entire process for Android. I was looking at months of research and development. Wait a minute! React Native. I know React. I've been wanting to learn React Native. It's already running a JavaScript process, so evaluating the input should be pretty simple. This would be a perfect project to get my feet wet.
I figured that with what I already knew about JavaScript and React, building this app would be a matter of a week, vs. months if I did it natively.
I was wrong. Very wrong.
I had a finished working version for both iOS and Android in about eight hours. This is having never done any mobile development, and having no experience with Xcode, or Android emulators.
iOS Version
At first I just wanted to see if I could get things working for iOS. I felt that trying to juggle both platforms would be challenging for a noob like myself.
I ran into a couple snags along the way, but none of it was React Native. After going through the getting started docs, my major impediments were with installed software being out of date. I first encountered an error when running my project in Xcode saying "Cannot read property 'root' of null"
. After some Googling this was easily solved by updating watchman brew update && brew upgrade watchman
. Next I had some error in Xcode about RCTWebSocketManager
. Thankfully an issue on react-native's GitHub informed me was due to Xcode being out of date. I needed to be running Xcode 6.3 or greater. I updated and had no further problems.
Once I had everything setup I set out building the interface first. This was surprisingly straight forward after working in React for a while, and the developer experience was great. Once Xcode was up and running, just press ⌘R
to reload changes (or enable live reloading by pressing ⌃⌘Z
and selecting "Enable Live Reloading").
I had to reference the docs periodically to know what elements I should use. It was basically ScrollView
for my container, View
where I would use div
in the browser, Text
where I would use a span
, and finally TextInput
instead of input
. My JSX stripped of all props looks something like this:
<ScrollView>
<Text>$ pocket-node</Text>
{this.state.io.map((row) => {
<Text>
{row.type === 'in' ? '> ' : ''}
{row.type === 'out' ? format(row.value) : row.value}
</Text>
})}
<View>
<Text>> </Text>
<TextInput/>
</View>
</ScrollView>
I then added some styles which was very similar to using inline styles in React for the browser. I did have to brush up on flexbox a bit, but all in all, it felt just like what I was already used to doing in React.
The last thing to do was evaluate the input and format the output. I was still a bit worried about what evaluating the input would involve, but it proved to be very straight forward (with the exception of a gotcha with evaluating object literals -- go ahead and try {a:'foo'}
and {a:'foo', b:'bar'}
in your browser's console):
function evaluate(code) {
var wrap = false;
var result;
// Wrap object literals, otherwise they are expressed as a block
if (code.indexOf('{') === 0 &&
code.indexOf('}') === code.length - 1) {
wrap = true;
}
// Evaluate code that was entered
try {
result = eval.call(null, (
wrap ? '(' : '') + code + (wrap ? ')' : '')
);
} catch (x) {
result = x;
}
return result;
}
Formatting the output of the above was simply a matter of doing a lot of type checking and styling the result accordingly. For this process I basically entered different types of data into a node REPL in my command line and styled it accordingly for my app.
It all just felt like using the React I was already familiar with. The only thing that felt significantly different was trying to keep the scroll to the bottom of the ScrollView
as new output was rendered. For this I had to track the orientation of the layout, import the UIManager
from NativeModules
, manually measure the outer and inner nodes of the ScrollView
, then scroll to that position.
handleLayoutChange: function (e) {
var layout = e.nativeEvent.layout;
var width = layout.width;
var height = layout.height;
orientation = width < height ? 'portrait' : 'landscape';
},
handleInputSubmitEditing: function (e) {
/* ... */
var outerNode = React.findNodeHandle(this.refs.scrollView);
var innerNode = this.refs.scrollView.getInnerViewNode();
UIManager.measure(outerNode, (oX, oY, oW, oH, oL, oT) => {
UIManager.measure(innerNode, (iX, iY, iW, iH, iL, iT) => {
// TODO: No idea where 200/30 is coming from
iH = (iH | 0) + (orientation === 'portrait' ? 200 : 30);
if (iH > oH) {
this.refs.scrollView.scrollTo(iH - oH);
}
});
});
}
That's it. I had a working iOS app in about five hours. My next task was to implement for Android.
Android Version
Working on Android required installing a JDK, which was a bit of a pain. I had difficulty finding a download for the latest version, and once I did, the installer was corrupted. I finally got that all sorted out. There was a bit of setup required, like setting environment variables, configuring the SDK, and creating an emulator. All of that is well documented.
I assumed that JSX for Android would be significantly different, so I started by copying the JSX from index.ios.js
to index.android.js
. All my evaluating/formatting code was already in their own files, and I was importing them into my main file, so it was easy to share this between the two platforms. I fired up an Android emulator and was pleasantly surprised to discover that everything worked. The only difference was some styling issues.
Since I was able to share the JSX between the two platforms, I moved everything out of index.ios.js
, and index.android.js
into a shared source file. Now my index files were very simple:
'use strict';
var React = require('react-native');
var {
AppRegistry,
StatusBarIOS,
} = React;
var PocketNode = require('./app/components/PocketNode');
StatusBarIOS.setStyle('light-content');
AppRegistry.registerComponent('PocketNode', () => PocketNode);
This is the iOS version, but the Android is identical with the exception of the StatusBarIOS
bit.
I didn't want to duplicate styles between iOS, and Android, so I used a shared styles.js
and detected the platform so I could use conditionals for where the styles needed to be different.
var React = require('react-native');
var {
Platform,
StyleSheet,
} = React;
var isAndroid = Platform.OS === 'android';
module.exports = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0E1628',
padding: 5,
paddingTop: isAndroid ? 5 : 25,
},
text: {
color: '#F4FFFF',
fontFamily: isAndroid ? 'monospace' : 'Courier New',
fontSize: 12,
},
view: {
flex: 1,
flexDirection: 'row',
marginTop: isAndroid ? - 6.75 : 0,
},
caret: {
width: isAndroid ? 11 : 14.25,
marginTop: isAndroid ? 6.75 : 0,
},
input: {
height: isAndroid ? 34 : 15,
flex: 1,
backgroundColor: 'transparent',
},
});
Once that was in place everything worked and looked just the way I wanted it to. All the JSX, logic, and styles were being shared across iOS and Android. The only exceptions were for six conditional style values, and I discovered that the scrolling logic mentioned previously didn't need to be executed for Android. Getting everything setup and working on Android took me about three hours.
To the App Store?
Once I was satisfied with my results I tweeted out a YouTube video demoing the app. The first response was asking when it would be in the App Store. At first I fully intended to release it. Once I looked at joining the Apple Developer Program however, I was very turned off. It costs $99/year, which doesn't seem worth it for releasing a free app.
I mentioned this to my friend Kent C. Dodds, and he asked why it needed to be a native app. Well, because I built a native app...duh. Even though I was admittedly a bit disappointed, after I thought about it, I realized that he was right. Building a native mobile app was a fun experience, but this isn't something that needs to be a native app. It would easily run in the browser and would have the benefit of easily releasing updates - while not having to give Apple any more of my money (glances back and forth between the two Mac Book Pros sitting on my desk, turns to my iPhone to see an iMessage from my wife asking if my kids can use my iPad, then randomly remembers I need to charge my iPod before I go running -- still maintains I'm not a fanboy).
Web Version
I thought that this would be a great experience in putting React/React Native to the test. The philosophy behind React Native is "Learn once, write anywhere". I felt that I had proven that to be valid in taking my React experience, and applying it to React Native. What would it be like, I wondered, to take my React Native code and port it to React?
This turned out to be ridiculously simple. I literally ported my app from React Native to React DOM in 30 minutes. I took notes as I went along, and here are the changes that I made:
npm i -S react react-dom react-style
- replace
require('react-native')
withrequire('react')
- replace
require('react-native').StyleSheet
withrequire('react-style')
- replace
Text
withspan
- replace
View
withdiv
- replace
ScrollView
withdiv
- replace
TextInput
withinput
- replace
\n
with<br/>
(this was being used in formatting) - replace
with
white-space: pre
(again used in formatting) - remove
Orientation
logic - remove
Platform
logic - refactor
onSubmitEditing
toonKeyUp
- refactor
e.nativeEvent.text
toe.target.value
- refactor
style={[styles.text, styles.caret]}
tofunction mergeStyles() {/*...*/}
- refactor CSS inconsistencies (most significant change)
Aside from these minor changes, I also had to setup a webpack config, a few npm scripts, and create an index.html
. All in all, very trivial stuff.
Takeaways
I learned several things in this process, that I'd like to leave as food for thought.
- React Native has made native mobile development very accessible to web developers. I have had ideas for mobile apps in the past, but never pursued them because I was intimidated by the learning curve. While I'm not necessarily going to start bidding on mobile app contracts, I now feel like I could.
- React Native allows your company to leverage your web developers to build mobile apps. If you're a web shop, mobile development might not be in your wheel house. This may lead you to outsourcing your mobile app. Why not instead let your web developers put their skills to work and build your mobile app? Not to mention with the code being in JavaScript, you can likely share some modules between your mobile and web app.
- You don't always need a mobile app. Eager as I was to build a mobile app, if given the choice between mobile and the web, choose the web. Some apps may require the capabilities of running natively. At the same time we continue pushing the boundaries of what the web can do. Ask yourself if your app justifies being native. In my case, it was not justifiable.
- React Native makes it very easy to change your mind regarding native vs. web. I was very easily able to refactor my React Native code to React DOM, and the inverse would also be true. Admittedly my code base was trivial compared to many apps. With a codemod tool like jscodeshift, you could easily automate much of the effort. Regardless, when using React, should you decide that your app doesn't really need to be native, or vice versa, you can refactor instead of rewrite.
Resources
I have been asked if I would share my source code. It is all available on my GitHub.
- React Native version - https://github.com/mzabriskie/PocketNode
- React DOM version - https://github.com/mzabriskie/pocket-node
Oh, and if you want to play with the final result, check out http://pocketnode.io.
Many thanks to Lin Clark, Kent C. Dodds, Christopher Chedeau, and Tyler McGinnis for reviewing this post.
Open source hacker. Community organizer. Co-organizer @ReactRally. Software Sommelier.