React Native is one of the best things to happen in the cross-platform mobile app development scene since the WebView. I can’t get enough of it. My current project (an iOS and Android application built with React Native and a heavily customized UI) would have taken at least twice as long to build as a cross-platform Xamarin app, and twice again as long to build as two independent native apps. It provides an incredible speed boost and brings native app development way closer to the sub-second write/refresh/results loop that web developers have enjoyed for years.
It also brings over many of the pitfalls from the web world that native developers haven’t had to worry about so much. Here are three of the pitfalls that my team fell into while building our app, three stories of how we pulled ourselves out, and three ways that you can avoid falling, too.
Pit #1: Treating Yoga Like a CSS Layout Engine
One of the coolest features of React Native is its web-like Yoga layout engine. Instead of fussing with storyboard constraints in Xcode or XAML layouts in Android Studio (😱), we define our app as a collection of components that bump and jostle each other around the screen according to something very, very close to the Flexbox layout model you might know from the web. This behavior is driven by Facebook’s Yoga library.
Yoga lets you do some really neat stuff like center components vertically with three lines of code or build type-safe conditional style sheets that remind you at compile time when you forget to finish styling the invalid version of a <TextInputField>
. As a long-time web developer, I lulled myself to sleep with a song that goes something like this: 🎵hush little web dev, don’t you worry, Flexbox will help you build this app in a hurry🎵.
While visions of negative margins danced in my head, I missed the fact that Yoga is a very, very different beast than the CSS layout engines I know. Especially when it comes to <Text>
and alignItems
.
<Text>
is a kind of quantum component. It exhibits both block-like and inline-like behavior depending on who’s looking at it and in what context.
There is, for instance, a style rule concerning alignItems: baseline
. To my uninitiated eye, it seemed to be designed to take a bunch of sibling <Text>
components of varying size and shift them around vertically so that their baselines all line up. As it happens, this only works when the <Text>
components are in their inline-like state, which is only visible when they’re surrounded by a parent <Text>
component. If they’re surrounded by an implicit or explicit <View>
component instead, the <Text>
s take on their block-level state and their baselines go all catawampus.
Getting out of this pit was fairly simple (in retrospect), but we had to fall into it several times before we consistently remembered that while Yoga waddles like a CSS engine, and quacks like a CSS engine, it is not, in actual fact, a CSS engine.
Fortunately, years of dealing with not-quite-CSS in Internet Explorer have given us all lots of techniques for working around weird issues. Once it settled in that Yoga was more like IE than Safari, we started recovering from rendering oddities much faster.
Pit #2: Treating Storybook Like a Replacement for Integration Tests
This one is a bit embarrassing, but since we’re friends, I’ll tell you anyway. Early on in the project, I was blinded by Storybook’s greatness, and I used snapshots and stories in several places where I should’ve used old-fashioned integration tests.
If you’re comfortable with app development and new to React Native, Storybook is about as incredible as a magic unicorn riding a jet-ski down an avalanche while singing “Eye of The Tiger” with a full orchestra (whose members are also jet-skiing down the avalanche). It brings mobile app development way closer to the ideal sub-second write/refresh/results workflow that web-developers have been lording over us for years. And if you combine it with Redux and a well-thought-out state model, Storybook comes awfully close to being able to reliably represent any possible state that your users can get themselves into.
Tap one key-chord, and five iOS simulators, two real iPhones, and an Android emulator or three switch to a new view in the application. Make a modification to one of that view’s components, hit ⌘S, and see all of those screens update live. It’s heady stuff.
In case it isn’t obvious, I got a bit carried away and let Storybook stories and a bit of manual testing replace integration testing for several key sections of the app. This worked so close to perfectly that we almost kept doing it. But after a few embarrassing app failures that would normally have been caught by an integration test (and on app login, no less 😞) we decided to add some integration tests to our existing Storybook suite.
I’m still fairly convinced that with just the right state model and set of stories, we could completely eliminate the value of integration tests for our app (hope springs eternal), but while we’re developing that perfect state model, it helps to have an old school integration test runner that’s following behind and pointing out when we break things.
Pit #3: Forgetting That Cross-Platform Does Not Mean Single-Platform
If you’re building an app that people will use on both Android and iOS, modern cross-platform development tools like Xamarin and React Native can be real time savers. While using heavily automated tools, it’s easy to forget that cross-platform means something closer to “…and also another platform” than to “a unified platform.” I fell into this pit in a couple of ways.
First, when evaluating libraries for our app, on more than one occasion, I would start using something that looked great on iOS, only to discover later that it had some really annoying limitations on Android. This isn’t the end of the world in React Native because of the many and varied escape hatches available to you as a developer, but it cost me time and energy that could have been better spent building features or planning architecture. The lesson here is simple. Before selecting a library, test it on both platforms, preferably on real devices.
Second, when building a UI, there is a strong temptation not to run the app on Android devices until after you have it mostly working on iOS. The Android emulator is prone to running away with system resources and beach-balling even the most well-spec’ed Macs.
In addition, both the emulator and actual Android devices have a bad habit of sometimes ignoring reload requests sent to them by the React Native packager. When building a screen for iOS, the workflow is: write, save, and watch the simulator reload automatically. Building a screen for Android is more like: write, save, look for the green reloading bar for a second, fail to see it, save again (repeat 0-5 times), watch the emulator reload “automatically,” realize that Android doesn’t support that feature (overflowing views 😭), rewrite, wait for your Mac to stop beach-balling, repeat…
This bugginess is infuriating when you’re making small UI modifications. To minimize its impact on my emotional state (and pace of development), I formed a habit of building features on iOS first. Then at the end, I launched them on Android to verify behavior. This often worked fine, but in a few cases I had to completely change my <View>
hierarchy after “finishing” a feature to work around a difference between iOS and Android.
You can avoid this pit pretty easily. Even though it’s painful at times, keep an Android emulator open and running while developing a feature. You’ll occasionally need to tap ⌘S more than once to get it going, and sometimes the emulator itself will run away with your RAM and need to be restarted, but those bugs are way less annoying than having to rebuild a feature because you forgot to test it on Android.
To successfully build a cross-platform app, you need familiarity with React Native and each of the native platforms on which it depends. React Native can save you a ton of time, but it will increase, not decrease, the skillset required to build the app.